Compare commits
124
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76f550843b | ||
|
|
15d8a8ba1f | ||
|
|
e878f51887 | ||
|
|
16b267d3a5 | ||
|
|
c156a49cc0 | ||
|
|
20ff76df9d | ||
|
|
ff90953f17 | ||
|
|
545926cfcf | ||
|
|
42f6c42ae1 | ||
|
|
456237ba4e | ||
|
|
e887c2308a | ||
|
|
d07f259829 | ||
|
|
566011c4e9 | ||
|
|
cda0ec3a4c | ||
|
|
d00889ff9b | ||
|
|
eafd97b9ad | ||
|
|
7a0e0bccf4 | ||
|
|
01401d56b8 | ||
|
|
f69b7f6a35 | ||
|
|
cb5316e6f5 | ||
|
|
25baff3013 | ||
|
|
85d2d34a90 | ||
|
|
f6d9b7566b | ||
|
|
84cac870d5 | ||
|
|
2884ab524d | ||
|
|
eac470cb68 | ||
|
|
a15b9837c6 | ||
|
|
562d723fcf | ||
|
|
2fdd5a72f0 | ||
|
|
3e26387b9d | ||
|
|
dc7d442ace | ||
|
|
839f1beb41 | ||
|
|
bbdadb0bf5 | ||
|
|
cadf68740a | ||
|
|
6ad0a6a29d | ||
|
|
77af9d2e93 | ||
|
|
b1147b8bf9 | ||
|
|
5ab230f77b | ||
|
|
d9cb81e71d | ||
|
|
0058772f61 | ||
|
|
bbc2e4073b | ||
|
|
7967b363c2 | ||
|
|
27dffe9fa6 | ||
|
|
7213bace08 | ||
|
|
2cbc667f30 | ||
|
|
978dc4cda6 | ||
|
|
a960e3aba2 | ||
|
|
ff1d473b81 | ||
|
|
bd384bde4a | ||
|
|
e7f052dfa0 | ||
|
|
9903c72ff5 | ||
|
|
d7d237d37b | ||
|
|
5d03f6f0f0 | ||
|
|
38e2a64255 | ||
|
|
1f56338697 | ||
|
|
172467c0a6 | ||
|
|
ff2b6dff2b | ||
|
|
9f600f87af | ||
|
|
7eeff7a396 | ||
|
|
afd6a12e7d | ||
|
|
125d70c4b1 | ||
|
|
5ebe46a81c | ||
|
|
19174ab3e5 | ||
|
|
7c953978ed | ||
|
|
db5441d0e2 | ||
|
|
1d529e3be4 | ||
|
|
23247d4319 | ||
|
|
216d9b2996 | ||
|
|
62f1e62a26 | ||
|
|
55bdae77f9 | ||
|
|
82998a7795 | ||
|
|
9fe8b7572f | ||
|
|
a424092fbd | ||
|
|
ad427dc765 | ||
|
|
116eb80fc9 | ||
|
|
c094a5fd73 | ||
|
|
206e28d1b9 | ||
|
|
2f1a164c26 | ||
|
|
124a8c0b07 | ||
|
|
188445c57a | ||
|
|
35f018623f | ||
|
|
d64526fed2 | ||
|
|
9a8a175f7c | ||
|
|
6ecb27d522 | ||
|
|
24b9b64979 | ||
|
|
a4e4e1313e | ||
|
|
990dc7875a | ||
|
|
69e98a51ec | ||
|
|
08fd09f59b | ||
|
|
6c421717f8 | ||
|
|
083db95151 | ||
|
|
b11ba68ff8 | ||
|
|
12a4112055 | ||
|
|
3063b9cf22 | ||
|
|
bd32854c7a | ||
|
|
0718e63e2f | ||
|
|
cdb70f9171 | ||
|
|
69e07975e3 | ||
|
|
c5c9a1a88b | ||
|
|
31efa791bd | ||
|
|
f8c0b57d21 | ||
|
|
605c969d2f | ||
|
|
ad7d4e54c3 | ||
|
|
c7689cc64a | ||
|
|
27c44a0fbd | ||
|
|
190d9dd2b1 | ||
|
|
49672db563 | ||
|
|
80603c9a42 | ||
|
|
f3b156f2d0 | ||
|
|
ec82efebb4 | ||
|
|
33201096b7 | ||
|
|
a1068f1d7e | ||
|
|
8dd846d6cd | ||
|
|
82175f166f | ||
|
|
3c99809e8e | ||
|
|
26db2b3a41 | ||
|
|
232a71c502 | ||
|
|
8df263fd93 | ||
|
|
d2f3335b1a | ||
|
|
75eca743b4 | ||
|
|
7bfd68e374 | ||
|
|
b2e2b11ef2 | ||
|
|
93b9abd508 | ||
|
|
888b399a4f |
No files matched your search
@@ -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://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! :)
|
Usage instructions can be found [here](https://checkmate.so/docs).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
|
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
|
||||||
|
|
||||||
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,12 +157,11 @@ 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. 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).
|
2. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
|
||||||
3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
|
3. Open an issue if you believe you've encountered a bug.
|
||||||
4. Open an issue if you believe you've encountered a bug.
|
4. Check for good-first-issue's if you are a newcomer.
|
||||||
5. Check for good-first-issue's if you are a newcomer.
|
5. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||||
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
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).
|
||||||
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" />
|
||||||
@@ -175,9 +174,3 @@ 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.
|
|
||||||
Generated
+141
-171
@@ -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.1.11"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"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.3.6"
|
"vite": "6.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -83,6 +83,7 @@
|
|||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -353,6 +354,7 @@
|
|||||||
"node_modules/@emotion/react": {
|
"node_modules/@emotion/react": {
|
||||||
"version": "11.14.0",
|
"version": "11.14.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -390,6 +392,7 @@
|
|||||||
"node_modules/@emotion/styled": {
|
"node_modules/@emotion/styled": {
|
||||||
"version": "11.14.1",
|
"version": "11.14.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -434,7 +437,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -451,7 +453,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -468,7 +469,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -485,7 +485,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -502,7 +501,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -519,7 +517,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -536,7 +533,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -553,7 +549,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -570,7 +565,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -587,7 +581,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -604,7 +597,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -621,7 +613,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -638,7 +629,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -655,7 +645,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -672,7 +661,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -689,7 +677,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -704,7 +691,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -721,7 +707,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -738,7 +723,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -755,7 +739,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -772,7 +755,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -789,7 +771,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -806,7 +787,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -823,7 +803,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -840,7 +819,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -857,7 +835,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1185,6 +1162,7 @@
|
|||||||
"node_modules/@mui/material": {
|
"node_modules/@mui/material": {
|
||||||
"version": "7.3.7",
|
"version": "7.3.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.4",
|
"@babel/runtime": "^7.28.4",
|
||||||
"@mui/core-downloads-tracker": "^7.3.7",
|
"@mui/core-downloads-tracker": "^7.3.7",
|
||||||
@@ -1289,6 +1267,7 @@
|
|||||||
"node_modules/@mui/system": {
|
"node_modules/@mui/system": {
|
||||||
"version": "7.3.7",
|
"version": "7.3.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.4",
|
"@babel/runtime": "^7.28.4",
|
||||||
"@mui/private-theming": "^7.3.7",
|
"@mui/private-theming": "^7.3.7",
|
||||||
@@ -1660,13 +1639,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||||
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
|
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1674,13 +1652,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||||
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
|
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1688,13 +1665,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||||
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
|
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1702,13 +1678,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||||
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
|
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1716,13 +1691,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||||
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
|
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1730,13 +1704,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||||
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
|
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1744,13 +1717,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||||
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
|
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1758,13 +1730,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||||
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
|
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1772,13 +1743,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
|
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1786,13 +1756,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||||
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
|
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1800,13 +1769,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
|
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1814,13 +1782,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||||
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
|
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1828,13 +1795,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
|
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1842,13 +1808,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||||
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
|
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1856,13 +1821,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
|
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1870,13 +1834,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||||
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
|
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1884,13 +1847,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
|
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1898,11 +1860,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1910,13 +1873,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||||
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
|
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1924,13 +1886,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||||
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
|
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1938,13 +1899,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||||
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
|
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1952,13 +1912,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||||
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
|
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1966,13 +1925,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||||
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
|
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1980,13 +1938,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||||
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
|
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1994,13 +1951,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||||
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
|
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2350,8 +2306,9 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.5.2",
|
"version": "24.5.2",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.12.0"
|
"undici-types": "~7.12.0"
|
||||||
}
|
}
|
||||||
@@ -2366,8 +2323,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.27",
|
"version": "18.3.27",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -2489,6 +2446,7 @@
|
|||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2505,7 +2463,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.14.0",
|
||||||
|
"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": {
|
||||||
@@ -2811,6 +2771,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -3242,7 +3203,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.13",
|
"version": "1.11.13",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
@@ -3566,7 +3528,6 @@
|
|||||||
},
|
},
|
||||||
"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": {
|
||||||
@@ -3625,6 +3586,7 @@
|
|||||||
"version": "8.57.1",
|
"version": "8.57.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@@ -3884,7 +3846,6 @@
|
|||||||
},
|
},
|
||||||
"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"
|
||||||
@@ -4018,7 +3979,6 @@
|
|||||||
"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,
|
||||||
@@ -4371,6 +4331,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.6"
|
"@babel/runtime": "^7.27.6"
|
||||||
},
|
},
|
||||||
@@ -5154,6 +5115,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
|
||||||
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
|
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/geojson-rewind": "^0.5.2",
|
"@mapbox/geojson-rewind": "^0.5.2",
|
||||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||||
@@ -5251,7 +5213,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
|
"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": {
|
||||||
@@ -5317,7 +5281,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -5650,7 +5613,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -5801,6 +5763,7 @@
|
|||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -5812,6 +5775,7 @@
|
|||||||
"node_modules/react-hook-form": {
|
"node_modules/react-hook-form": {
|
||||||
"version": "7.71.1",
|
"version": "7.71.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@@ -6011,7 +5975,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/redux": {
|
"node_modules/redux": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/redux-persist": {
|
"node_modules/redux-persist": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
@@ -6157,9 +6122,11 @@
|
|||||||
"license": "Unlicense"
|
"license": "Unlicense"
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.57.1",
|
"version": "4.59.0",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -6171,31 +6138,31 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.57.1",
|
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||||
"@rollup/rollup-android-arm64": "4.57.1",
|
"@rollup/rollup-android-arm64": "4.59.0",
|
||||||
"@rollup/rollup-darwin-arm64": "4.57.1",
|
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||||
"@rollup/rollup-darwin-x64": "4.57.1",
|
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||||
"@rollup/rollup-freebsd-arm64": "4.57.1",
|
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||||
"@rollup/rollup-freebsd-x64": "4.57.1",
|
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
|
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
|
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.57.1",
|
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||||
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
|
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||||
"@rollup/rollup-linux-loong64-musl": "4.57.1",
|
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||||
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
|
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||||
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
|
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
|
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||||
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
|
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
|
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.57.1",
|
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.57.1",
|
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||||
"@rollup/rollup-openbsd-x64": "4.57.1",
|
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||||
"@rollup/rollup-openharmony-arm64": "4.57.1",
|
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
|
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
|
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||||
"@rollup/rollup-win32-x64-gnu": "4.57.1",
|
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.57.1",
|
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6530,7 +6497,6 @@
|
|||||||
},
|
},
|
||||||
"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"
|
||||||
@@ -6818,7 +6784,6 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -6948,8 +6913,9 @@
|
|||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -6992,7 +6958,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.12.0",
|
"version": "7.12.0",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/union-value": {
|
"node_modules/union-value": {
|
||||||
@@ -7081,9 +7047,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.6",
|
"version": "6.4.1",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@@ -7366,7 +7334,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==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"yaml": "bin.mjs"
|
"yaml": "bin.mjs"
|
||||||
@@ -7391,6 +7359,8 @@
|
|||||||
},
|
},
|
||||||
"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
@@ -13,7 +13,7 @@
|
|||||||
"format-check": "prettier --check ."
|
"format-check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*": "npm run format"
|
"**/*": "prettier --write"
|
||||||
},
|
},
|
||||||
"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.1.11"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"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.3.6"
|
"vite": "6.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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).reverse();
|
const latestChecks = checks.slice(-25);
|
||||||
|
|
||||||
let data: Array<CheckSnapshot | PlaceholderCheck>;
|
let data: Array<CheckSnapshot | PlaceholderCheck>;
|
||||||
if (latestChecks.length !== 25) {
|
if (latestChecks.length !== 25) {
|
||||||
|
|||||||
@@ -11,20 +11,26 @@ 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, ...props }, ref) => {
|
({ sx, showValue = false, formatDisplayValue, ...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>{props.value}</Typography>}
|
{showValue && <Typography>{displayValue}</Typography>}
|
||||||
<Slider
|
<Slider
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -63,12 +69,21 @@ 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();
|
||||||
@@ -82,6 +97,7 @@ 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,9 +1,6 @@
|
|||||||
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";
|
||||||
|
|
||||||
@@ -13,7 +10,6 @@ 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);
|
||||||
|
|
||||||
@@ -47,10 +43,6 @@ 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"];
|
const types = ["http", "ping", "port", "docker", "game", "grpc", "websocket"];
|
||||||
const typeDisplayNames: Record<string, string> = {
|
const typeDisplayNames: Record<string, string> = {
|
||||||
http: "HTTP",
|
http: "HTTP",
|
||||||
ping: "Ping",
|
ping: "Ping",
|
||||||
@@ -15,6 +15,7 @@ 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"];
|
||||||
|
|||||||
@@ -103,6 +103,14 @@ 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,
|
||||||
|
|||||||
@@ -11,12 +11,17 @@ 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 } from "react-router-dom";
|
import { useParams, useNavigate } 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 } from "@/Hooks/UseApi";
|
import { useGet, usePatch, useDelete } from "@/Hooks/UseApi";
|
||||||
import type { EditUserFormData } from "@/Validation/editUser";
|
import type { EditUserFormData } from "@/Validation/editUser";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { DialogInput } from "@/Components/inputs/Dialog";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import type { RootState } from "@/Types/state";
|
||||||
|
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||||
|
import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin";
|
||||||
|
|
||||||
interface RoleOption {
|
interface RoleOption {
|
||||||
id: UserRole;
|
id: UserRole;
|
||||||
@@ -29,6 +34,12 @@ 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}`);
|
||||||
|
|
||||||
@@ -60,6 +71,20 @@ 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);
|
||||||
};
|
};
|
||||||
@@ -165,17 +190,45 @@ const EditUserPage = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Stack
|
||||||
type="submit"
|
gap={LAYOUT.XS}
|
||||||
variant="contained"
|
direction="row"
|
||||||
color="primary"
|
justifyContent={"flex-end"}
|
||||||
loading={isSaving}
|
width="100%"
|
||||||
sx={{ alignSelf: "flex-end", minWidth: 100 }}
|
|
||||||
>
|
>
|
||||||
{t("common.buttons.save")}
|
{canDeleteUser && (
|
||||||
</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 { useIsSuperAdmin } from "@/Hooks/useIsAdmin";
|
import { useIsAdmin } 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 isSuperAdmin = useIsSuperAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
|
|
||||||
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 (isSuperAdmin) {
|
if (isAdmin) {
|
||||||
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={isSuperAdmin ? handleRowClick : undefined}
|
onRowClick={isAdmin ? handleRowClick : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -32,7 +32,12 @@ 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 type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
|
import {
|
||||||
|
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";
|
||||||
|
|
||||||
@@ -141,6 +146,17 @@ 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;
|
||||||
};
|
};
|
||||||
@@ -326,6 +342,13 @@ const CreateMonitorPage = () => {
|
|||||||
"pages.createMonitor.form.type.optionGrpcDescription"
|
"pages.createMonitor.form.type.optionGrpcDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<RadioWithDescription
|
||||||
|
value="websocket"
|
||||||
|
label={t("pages.createMonitor.form.type.optionWebSocket")}
|
||||||
|
description={t(
|
||||||
|
"pages.createMonitor.form.type.optionWebSocketDescription"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -742,7 +765,9 @@ const CreateMonitorPage = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(watchedType === "http" || watchedType === "grpc") && (
|
{(watchedType === "http" ||
|
||||||
|
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")}
|
||||||
@@ -885,7 +910,7 @@ const CreateMonitorPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{watchedType === "http" && (
|
{supportsGeoCheck(watchedType) && (
|
||||||
<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")}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ 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;
|
||||||
@@ -462,6 +463,43 @@ 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
|
||||||
|
|||||||
@@ -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 } from "@/Types/Monitor";
|
import { type MonitorDetailsResponse, supportsGeoCheck } 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 || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
|
if (!monitorId || !supportsGeoCheck(monitor?.type) || !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 || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
|
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const UptimeMonitorsPage = () => {
|
|||||||
const effectiveTypes =
|
const effectiveTypes =
|
||||||
selectedTypes.length > 0
|
selectedTypes.length > 0
|
||||||
? selectedTypes
|
? selectedTypes
|
||||||
: ["http", "ping", "docker", "port", "game", "grpc"];
|
: ["http", "ping", "docker", "port", "game", "grpc", "websocket"];
|
||||||
|
|
||||||
// Build URL for monitors with checks
|
// Build URL for monitors with checks
|
||||||
const monitorsWithChecksUrl = useMemo(() => {
|
const monitorsWithChecksUrl = useMemo(() => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export const CHECK_TTL_SENTINEL = 366;
|
||||||
|
|
||||||
export interface CheckMetadata {
|
export interface CheckMetadata {
|
||||||
monitorId: string;
|
monitorId: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -125,9 +127,6 @@ 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[];
|
||||||
@@ -140,7 +139,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -243,10 +241,7 @@ export interface ChecksSummary {
|
|||||||
downChecks: number;
|
downChecks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CheckSnapshot = Omit<
|
export type CheckSnapshot = Omit<Check, "metadata" | "__v" | "updatedAt"> & {
|
||||||
Check,
|
|
||||||
"metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt"
|
|
||||||
> & {
|
|
||||||
originalResponseTime: number;
|
originalResponseTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,20 @@ 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",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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];
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ 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",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,4 +33,8 @@ export const darkPalette = {
|
|||||||
secondary: {
|
secondary: {
|
||||||
main: colors.gray700,
|
main: colors.gray700,
|
||||||
},
|
},
|
||||||
|
background: {
|
||||||
|
default: colors.gray900,
|
||||||
|
paper: colors.gray850,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@@ -115,6 +115,13 @@ 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,
|
||||||
@@ -125,6 +132,7 @@ 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>;
|
||||||
@@ -139,4 +147,5 @@ export {
|
|||||||
grpcSchema,
|
grpcSchema,
|
||||||
pagespeedSchema,
|
pagespeedSchema,
|
||||||
hardwareSchema,
|
hardwareSchema,
|
||||||
|
websocketSchema,
|
||||||
};
|
};
|
||||||
@@ -45,6 +45,11 @@ 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,
|
||||||
@@ -52,6 +57,7 @@ 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>;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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({
|
||||||
@@ -11,7 +12,11 @@ export const settingsSchema = z.object({
|
|||||||
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.number().int().min(1, "Please enter a value").max(365, "Maximum 365 days"),
|
checkTTL: z
|
||||||
|
.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() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
||||||
|
|||||||
@@ -46,7 +46,8 @@
|
|||||||
"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": {
|
||||||
@@ -487,6 +488,10 @@
|
|||||||
"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",
|
||||||
@@ -498,6 +503,7 @@
|
|||||||
"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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -534,6 +540,8 @@
|
|||||||
"optionGameDescription": "Monitor if a specific game server is online.",
|
"optionGameDescription": "Monitor if a specific game server is online.",
|
||||||
"optionGrpc": "gRPC",
|
"optionGrpc": "gRPC",
|
||||||
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
||||||
|
"optionWebSocket": "WebSocket",
|
||||||
|
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
||||||
"optionHttp": "HTTP(S)",
|
"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",
|
"optionPing": "Ping",
|
||||||
@@ -619,6 +627,12 @@
|
|||||||
"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": {
|
||||||
@@ -1027,7 +1041,7 @@
|
|||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"title": "Monitor history",
|
"title": "Monitor history",
|
||||||
"description": "Define how long you want to retain historical data. You can also clear all existing data.",
|
"description": "Clear all monitoring history and statistics for your team. This action is irreversible.",
|
||||||
"option": {
|
"option": {
|
||||||
"clear": {
|
"clear": {
|
||||||
"label": "Clear all stats. This is irreversible."
|
"label": "Clear all stats. This is irreversible."
|
||||||
@@ -1038,6 +1052,16 @@
|
|||||||
"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.",
|
||||||
|
|||||||
@@ -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.0";
|
let version = "3.5.1";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base: "/",
|
base: "/",
|
||||||
|
|||||||
Generated
+657
-705
File diff suppressed because it is too large.
Load diff
+5
-3
@@ -15,7 +15,7 @@
|
|||||||
"format-check": "prettier --check ."
|
"format-check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*": "npm run format"
|
"**/*": "prettier --write"
|
||||||
},
|
},
|
||||||
"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.3.1",
|
"gamedig": "5.2.0",
|
||||||
"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,13 +51,14 @@
|
|||||||
"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": "^6.9.14",
|
"nodemailer": "8.0.1",
|
||||||
"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": {
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
"@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",
|
||||||
|
|||||||
+16
-2
@@ -74,8 +74,22 @@ export const createApp = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
// Swagger UI
|
// Swagger UI — dynamically set server URL from request
|
||||||
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));
|
app.use("/api-docs", swaggerUi.serve, (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
const protocol = req.protocol;
|
||||||
|
const host = req.get("host");
|
||||||
|
const dynamicSpec = {
|
||||||
|
...openApiSpec,
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: `${protocol}://${host}/api/v1`,
|
||||||
|
description: "Current Server",
|
||||||
|
},
|
||||||
|
...openApiSpec.servers,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
swaggerUi.setup(dynamicSpec)(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
app.use("/api/v1/health", (req, res) => {
|
app.use("/api/v1/health", (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -1,35 +1,70 @@
|
|||||||
import MongoDB from "../db/MongoDB.js";
|
import MongoDB from "../db/MongoDB.js";
|
||||||
import NetworkService from "../service/infrastructure/networkService.js";
|
import { IDb } from "@/db/IDb.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";
|
|
||||||
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
// Network providers
|
||||||
import UserService from "../service/business/userService.js";
|
import { PingProvider } from "@/service/infrastructure/network/PingProvider.js";
|
||||||
import CheckService from "../service/business/checkService.js";
|
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
|
||||||
import GeoChecksService from "../service/business/geoChecksService.js";
|
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
|
||||||
import GlobalPingService from "../service/infrastructure/globalPingService.js";
|
import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js";
|
||||||
import DiagnosticService from "../service/business/diagnosticService.js";
|
import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js";
|
||||||
import InviteService from "../service/business/inviteService.js";
|
import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js";
|
||||||
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
|
import { PortProvider } from "@/service/infrastructure/network/PortProvider.js";
|
||||||
import { MonitorService } from "@/service/index.js";
|
import { GameProvider } from "@/service/infrastructure/network/GameProvider.js";
|
||||||
import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js";
|
import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js";
|
||||||
import IncidentService from "../service/business/incidentService.js";
|
import { WebSocketProvider } from "@/service/infrastructure/network/WebSocketProvider.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";
|
||||||
@@ -44,8 +79,9 @@ 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,
|
||||||
@@ -74,24 +110,23 @@ 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: any;
|
settingsService: ISettingsService;
|
||||||
db: any;
|
db: IDb;
|
||||||
networkService: any;
|
networkService: INetworkService;
|
||||||
emailService: any;
|
emailService: IEmailService;
|
||||||
bufferService: any;
|
bufferService: IBufferService;
|
||||||
statusService: any;
|
statusService: IStatusService;
|
||||||
jobQueue: any;
|
jobQueue: ISuperSimpleQueue;
|
||||||
userService: any;
|
userService: IUserService;
|
||||||
checkService: any;
|
checkService: ICheckService;
|
||||||
geoChecksService: any;
|
geoChecksService: IGeoChecksService;
|
||||||
diagnosticService: any;
|
diagnosticService: IDiagnosticService;
|
||||||
inviteService: any;
|
inviteService: IInviteService;
|
||||||
maintenanceWindowService: any;
|
maintenanceWindowService: IMaintenanceWindowService;
|
||||||
monitorService: any;
|
monitorService: IMonitorService;
|
||||||
incidentService: any;
|
incidentService: IIncidentService;
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
notificationsService: INotificationsService;
|
notificationsService: INotificationsService;
|
||||||
statusPageService: IStatusPageService;
|
statusPageService: IStatusPageService;
|
||||||
@@ -121,7 +156,7 @@ export const initializeServices = async ({
|
|||||||
}: {
|
}: {
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
envSettings: EnvConfig;
|
envSettings: EnvConfig;
|
||||||
settingsService: any;
|
settingsService: ISettingsService;
|
||||||
settingsRepository: ISettingsRepository;
|
settingsRepository: ISettingsRepository;
|
||||||
}): Promise<InitializedServices> => {
|
}): Promise<InitializedServices> => {
|
||||||
// Create DB
|
// Create DB
|
||||||
@@ -144,22 +179,28 @@ export const initializeServices = async ({
|
|||||||
const teamsRepository = new MongoTeamsRepository();
|
const teamsRepository = new MongoTeamsRepository();
|
||||||
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
||||||
|
|
||||||
const networkService = new NetworkService(
|
// Network providers
|
||||||
axios,
|
const pingProvider = new PingProvider(ping);
|
||||||
got,
|
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath));
|
||||||
https,
|
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger);
|
||||||
jmespath,
|
const hardwareProvider = new HardwareProvider(httpProvider);
|
||||||
GameDig as unknown as {
|
const dockerProvider = new DockerProvider(logger, Docker);
|
||||||
query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }>;
|
const portProvider = new PortProvider(net);
|
||||||
},
|
const gameProvider = new GameProvider(logger, GameDig);
|
||||||
ping,
|
const grpcProvider = new GrpcProvider(grpc, protoLoader);
|
||||||
logger,
|
const webSocketProvider = new WebSocketProvider(WebSocket);
|
||||||
Docker,
|
|
||||||
net,
|
const networkService = new NetworkService(axios, logger, [
|
||||||
settingsService,
|
pingProvider,
|
||||||
grpc,
|
httpProvider,
|
||||||
protoLoader
|
pageSpeedProvider,
|
||||||
);
|
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();
|
||||||
@@ -181,12 +222,14 @@ 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,
|
||||||
@@ -197,6 +240,7 @@ export const initializeServices = async ({
|
|||||||
discordProvider,
|
discordProvider,
|
||||||
pagerDutyProvider,
|
pagerDutyProvider,
|
||||||
matrixProvider,
|
matrixProvider,
|
||||||
|
teamsProvider,
|
||||||
settingsService,
|
settingsService,
|
||||||
logger,
|
logger,
|
||||||
notificationMessageBuilder
|
notificationMessageBuilder
|
||||||
@@ -208,6 +252,7 @@ export const initializeServices = async ({
|
|||||||
statusService,
|
statusService,
|
||||||
notificationsService,
|
notificationsService,
|
||||||
checkService,
|
checkService,
|
||||||
|
settingsService,
|
||||||
bufferService,
|
bufferService,
|
||||||
incidentService,
|
incidentService,
|
||||||
maintenanceWindowsRepository,
|
maintenanceWindowsRepository,
|
||||||
@@ -264,7 +309,6 @@ 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,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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, requireUserRoles } from "./controllerUtils.js";
|
import { requireTeamId, requireUserEmail, requireUserId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
||||||
import type { UserRole } from "@/types/user.js";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
registrationBodyValidation,
|
registrationBodyValidation,
|
||||||
@@ -20,15 +19,16 @@ 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";
|
||||||
|
|
||||||
class AuthController {
|
class AuthController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private userService: any;
|
private userService: IUserService;
|
||||||
|
|
||||||
constructor(userService: any) {
|
constructor(userService: IUserService) {
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,11 +41,12 @@ class AuthController {
|
|||||||
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) {
|
||||||
newUser.email = newUser.email.toLowerCase();
|
const newUserEmail = requireUserEmail(newUser.email);
|
||||||
|
newUser.email = newUserEmail.toLowerCase();
|
||||||
}
|
}
|
||||||
registrationBodyValidation.parse(newUser);
|
const validatedBody = registrationBodyValidation.parse(newUser);
|
||||||
|
|
||||||
const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file);
|
const { user, token } = await this.userService.registerUser(validatedBody, newUserToken, req?.file ?? null);
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "User registered successfully",
|
msg: "User registered successfully",
|
||||||
@@ -62,11 +63,11 @@ class AuthController {
|
|||||||
if (userData?.email) {
|
if (userData?.email) {
|
||||||
userData.email = userData.email.toLowerCase();
|
userData.email = userData.email.toLowerCase();
|
||||||
}
|
}
|
||||||
createUserBodyValidation.parse(userData);
|
const validatedBody = createUserBodyValidation.parse(userData);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const actorRoles = requireUserRoles(req.user?.role) as UserRole[];
|
const actorRoles = requireUserRoles(req.user?.role);
|
||||||
const newUser = await this.userService.createUser(userData, teamId, actorRoles, req.file);
|
const newUser = await this.userService.createUser(validatedBody, teamId, actorRoles, req?.file ?? null);
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "User created successfully",
|
msg: "User created successfully",
|
||||||
@@ -100,8 +101,10 @@ class AuthController {
|
|||||||
|
|
||||||
editUser = async (req: Request, res: Response, next: NextFunction) => {
|
editUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
editUserBodyValidation.parse(req.body);
|
const validatedBody = editUserBodyValidation.parse(req.body);
|
||||||
const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
|
const userId = requireUserId(req.user?.id);
|
||||||
|
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,
|
||||||
@@ -170,7 +173,15 @@ class AuthController {
|
|||||||
|
|
||||||
deleteUser = async (req: Request, res: Response, next: NextFunction) => {
|
deleteUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await this.userService.deleteUser(req.user);
|
const userId = requireUserId(req.user?.id);
|
||||||
|
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",
|
||||||
@@ -180,6 +191,23 @@ class AuthController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
@@ -195,19 +223,9 @@ class AuthController {
|
|||||||
|
|
||||||
getUserById = async (req: Request, res: Response, next: NextFunction) => {
|
getUserById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getUserByIdParamValidation.parse(req.params);
|
const validatedParams = getUserByIdParamValidation.parse(req.params);
|
||||||
const userId = req?.params?.userId;
|
const actorRoles = requireUserRoles(req.user?.role);
|
||||||
const roles = req?.user?.role;
|
const user = await this.userService.getUserById(actorRoles, validatedParams.userId);
|
||||||
|
|
||||||
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) {
|
||||||
@@ -217,24 +235,19 @@ class AuthController {
|
|||||||
|
|
||||||
editUserById = async (req: Request, res: Response, next: NextFunction) => {
|
editUserById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const roles = req?.user?.role;
|
const actorRoles = requireUserRoles(req.user?.role);
|
||||||
|
const actorId = requireUserId(req.user?.id);
|
||||||
|
|
||||||
if (!roles || !roles.includes("superadmin")) {
|
if (!actorRoles.includes("superadmin")) {
|
||||||
throw new AppError({ message: "Unauthorized", status: 403 });
|
throw new AppError({ message: "Unauthorized", status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = req.params.userId as string;
|
const validatedParams = editUserByIdParamValidation.parse(req.params);
|
||||||
const user = { ...req.body };
|
|
||||||
|
|
||||||
editUserByIdParamValidation.parse(req.params);
|
|
||||||
// If this is superadmin self edit, allow "superadmin" role
|
// If this is superadmin self edit, allow "superadmin" role
|
||||||
if (userId === req.user?.id) {
|
const validatedBody =
|
||||||
editSuperadminUserByIdBodyValidation.parse(req.body);
|
validatedParams.userId === actorId ? editSuperadminUserByIdBodyValidation.parse(req.body) : editUserByIdBodyValidation.parse(req.body);
|
||||||
} else {
|
|
||||||
editUserByIdBodyValidation.parse(req.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.userService.editUserById(userId, user);
|
await this.userService.editUserById(validatedParams.userId, validatedBody);
|
||||||
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);
|
||||||
@@ -243,16 +256,14 @@ class AuthController {
|
|||||||
|
|
||||||
editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => {
|
editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const roles = req?.user?.role;
|
const actorRoles = requireUserRoles(req.user?.role);
|
||||||
if (!roles || !roles.includes("superadmin")) {
|
if (!actorRoles.includes("superadmin")) {
|
||||||
throw new AppError({ message: "Unauthorized", status: 403 });
|
throw new AppError({ message: "Unauthorized", status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = req.params.userId as string;
|
const validatedParams = editUserByIdParamValidation.parse(req.params);
|
||||||
editUserByIdParamValidation.parse(req.params);
|
const validatedBody = editUserPasswordByIdBodyValidation.parse(req.body);
|
||||||
editUserPasswordByIdBodyValidation.parse(req.body);
|
await this.userService.setPasswordByUserId(validatedParams.userId, validatedBody.password);
|
||||||
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);
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ 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";
|
||||||
|
|
||||||
class CheckController {
|
class CheckController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private checkService: any;
|
private checkService: ICheckService;
|
||||||
constructor(checkService: any) {
|
constructor(checkService: ICheckService) {
|
||||||
this.checkService = checkService;
|
this.checkService = checkService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,13 +26,20 @@ class CheckController {
|
|||||||
|
|
||||||
getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getChecksParamValidation.parse(req.params);
|
const validatedParams = getChecksParamValidation.parse(req.params);
|
||||||
getChecksQueryValidation.parse(req.query);
|
const validatedQuery = getChecksQueryValidation.parse(req.query);
|
||||||
|
|
||||||
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const result = await this.checkService.getChecksByMonitor({
|
const result = await this.checkService.getChecksByMonitor({
|
||||||
monitorId: req?.params?.monitorId,
|
monitorId: validatedParams.monitorId,
|
||||||
query: req?.query,
|
teamId,
|
||||||
teamId: req?.user?.teamId,
|
sortOrder: validatedQuery.sortOrder,
|
||||||
|
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({
|
||||||
@@ -46,10 +54,16 @@ class CheckController {
|
|||||||
|
|
||||||
getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => {
|
getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getTeamChecksQueryValidation.parse(req.query);
|
const validatedQuery = getTeamChecksQueryValidation.parse(req.query);
|
||||||
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const checkData = await this.checkService.getChecksByTeam({
|
const checkData = await this.checkService.getChecksByTeam({
|
||||||
teamId: req?.user?.teamId,
|
teamId,
|
||||||
query: req?.query,
|
sortOrder: validatedQuery.sortOrder,
|
||||||
|
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,
|
||||||
@@ -63,8 +77,11 @@ class CheckController {
|
|||||||
|
|
||||||
getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const dateRange = req.query?.dateRange || "hour";
|
const validatedQuery = getChecksSummaryByTeamIdQueryValidation.parse(req.query);
|
||||||
const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId, dateRange });
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
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",
|
||||||
@@ -77,11 +94,12 @@ class CheckController {
|
|||||||
|
|
||||||
deleteChecks = async (req: Request, res: Response, next: NextFunction) => {
|
deleteChecks = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
deleteChecksParamValidation.parse(req.params);
|
const validatedParams = deleteChecksParamValidation.parse(req.params);
|
||||||
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const deletedCount = await this.checkService.deleteChecks({
|
const deletedCount = await this.checkService.deleteChecks({
|
||||||
monitorId: req.params.monitorId as string,
|
monitorId: validatedParams.monitorId,
|
||||||
teamId: req?.user?.teamId,
|
teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -96,9 +114,9 @@ class CheckController {
|
|||||||
|
|
||||||
deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
deleteChecksByTeamIdParamValidation.parse(req.params);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId });
|
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -109,24 +127,6 @@ class CheckController {
|
|||||||
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;
|
||||||
@@ -1,24 +1,25 @@
|
|||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { type MonitorType, MonitorTypes } from "@/types/index.js";
|
import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index.js";
|
||||||
|
import sslChecker, { SSLDetails } from "ssl-checker";
|
||||||
|
type SSLCheckerType = typeof sslChecker;
|
||||||
|
|
||||||
const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise<any> => {
|
export const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise<SSLDetails> => {
|
||||||
const monitorUrl = new URL(monitor.url);
|
const monitorUrl = new URL(monitor.url);
|
||||||
const hostname = monitorUrl.hostname;
|
const hostname = monitorUrl.hostname;
|
||||||
const cert = await sslChecker(hostname);
|
const cert = await checker(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;
|
||||||
};
|
};
|
||||||
const requireString = (value: unknown, fieldName: string): string => {
|
export 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 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const optionalString = (value: unknown, fieldName: string): string | undefined => {
|
export const optionalString = (value: unknown, fieldName: string): string | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -28,7 +29,7 @@ const optionalString = (value: unknown, fieldName: string): string | undefined =
|
|||||||
throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
|
throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
|
export const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -44,7 +45,7 @@ const optionalNumber = (value: unknown, fieldName: string): number | undefined =
|
|||||||
throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
|
throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
|
export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -62,7 +63,7 @@ const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined
|
|||||||
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
|
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
|
export 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 });
|
||||||
@@ -82,7 +83,7 @@ const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | u
|
|||||||
return parseSingle(value);
|
return parseSingle(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -92,42 +93,36 @@ 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 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const requireTeamId = (teamId?: string): string => {
|
export 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
const requireUserId = (userId?: string): string => {
|
export 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;
|
||||||
};
|
};
|
||||||
const requireUserEmail = (userEmail?: string): string => {
|
export 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 requireUserRoles = (userRoles?: string[]): string[] => {
|
export const requireFirstName = (firstName?: 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,12 +1,13 @@
|
|||||||
|
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 {
|
class DiagnosticController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private diagnosticService: any;
|
private diagnosticService: IDiagnosticService;
|
||||||
|
|
||||||
constructor(diagnosticService: any) {
|
constructor(diagnosticService: IDiagnosticService) {
|
||||||
this.diagnosticService = diagnosticService;
|
this.diagnosticService = diagnosticService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,30 @@
|
|||||||
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";
|
||||||
|
|
||||||
class IncidentController {
|
class IncidentController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
private incidentService: IIncidentService;
|
||||||
|
constructor(incidentService: IIncidentService) {
|
||||||
private incidentService: any;
|
|
||||||
constructor(incidentService: any) {
|
|
||||||
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 { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {};
|
const validatedQuery = getIncidentsByTeamQueryValidation.parse(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,
|
||||||
sortOrder,
|
validatedQuery.sortOrder,
|
||||||
dateRange,
|
validatedQuery.dateRange,
|
||||||
page,
|
validatedQuery.page,
|
||||||
rowsPerPage,
|
validatedQuery.rowsPerPage,
|
||||||
status,
|
validatedQuery.status,
|
||||||
monitorId,
|
validatedQuery.monitorId,
|
||||||
resolutionType
|
validatedQuery.resolutionType
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -45,10 +40,9 @@ class IncidentController {
|
|||||||
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 limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10;
|
const summary = await this.incidentService.getIncidentSummary(teamId, validatedQuery.limit);
|
||||||
|
|
||||||
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",
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
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 { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
import { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
||||||
|
import { IInviteService } from "@/service/index.js";
|
||||||
const SERVICE_NAME = "inviteController";
|
const SERVICE_NAME = "inviteController";
|
||||||
|
|
||||||
class InviteController {
|
class InviteController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private inviteService: any;
|
private inviteService: IInviteService;
|
||||||
constructor(inviteService: any) {
|
constructor(inviteService: IInviteService) {
|
||||||
this.inviteService = inviteService;
|
this.inviteService = inviteService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,8 +35,9 @@ class InviteController {
|
|||||||
|
|
||||||
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;
|
||||||
@@ -43,7 +45,7 @@ class InviteController {
|
|||||||
|
|
||||||
const inviteToken = await this.inviteService.sendInviteEmail({
|
const inviteToken = await this.inviteService.sendInviteEmail({
|
||||||
invite: inviteRequest,
|
invite: inviteRequest,
|
||||||
firstName: req?.user?.firstName,
|
firstName,
|
||||||
userRoles,
|
userRoles,
|
||||||
});
|
});
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
class LogController {
|
class LogController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
constructor(logger: any) {
|
constructor(logger: ILogger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
get serviceName() {
|
get serviceName() {
|
||||||
@@ -14,7 +15,7 @@ class LogController {
|
|||||||
|
|
||||||
getLogs = async (req: Request, res: Response, next: NextFunction) => {
|
getLogs = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const logs = await this.logger.getLogs();
|
const logs = this.logger.getLogs();
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "Logs fetched successfully",
|
msg: "Logs fetched successfully",
|
||||||
|
|||||||
@@ -9,26 +9,19 @@ 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";
|
||||||
const SERVICE_NAME = "maintenanceWindowController";
|
|
||||||
|
|
||||||
class MaintenanceWindowController {
|
class MaintenanceWindowController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
private maintenanceWindowService: IMaintenanceWindowService;
|
||||||
private maintenanceWindowService: any;
|
constructor(maintenanceWindowService: IMaintenanceWindowService) {
|
||||||
constructor(maintenanceWindowService: any) {
|
|
||||||
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 {
|
||||||
createMaintenanceWindowBodyValidation.parse(req.body);
|
const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body);
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: validatedBody });
|
||||||
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
|
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -40,11 +33,11 @@ class MaintenanceWindowController {
|
|||||||
};
|
};
|
||||||
getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => {
|
getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getMaintenanceWindowByIdParamValidation.parse(req.params);
|
const validatedParams = getMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id as string, teamId });
|
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: validatedParams.id, teamId });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -58,11 +51,18 @@ class MaintenanceWindowController {
|
|||||||
|
|
||||||
getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query);
|
const validatedQuery = getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query });
|
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({
|
||||||
|
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,
|
||||||
@@ -76,12 +76,12 @@ class MaintenanceWindowController {
|
|||||||
|
|
||||||
getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params);
|
const validatedParams = 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: req.params.monitorId as string,
|
monitorId: validatedParams.monitorId,
|
||||||
teamId,
|
teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,11 +96,11 @@ class MaintenanceWindowController {
|
|||||||
};
|
};
|
||||||
deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
deleteMaintenanceWindowByIdParamValidation.parse(req.params);
|
const validatedParams = deleteMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id as string, teamId });
|
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: validatedParams.id, teamId });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -113,14 +113,14 @@ class MaintenanceWindowController {
|
|||||||
|
|
||||||
editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
editMaintenanceWindowByIdParamValidation.parse(req.params);
|
const validatedParams = editMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||||
editMaintenanceByIdWindowBodyValidation.parse(req.body);
|
const validatedBody = 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: req.params.id as string,
|
id: validatedParams.id,
|
||||||
body: req.body,
|
body: validatedBody,
|
||||||
teamId,
|
teamId,
|
||||||
});
|
});
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Request, Response, NextFunction } from "express";
|
|||||||
import {
|
import {
|
||||||
getMonitorByIdParamValidation,
|
getMonitorByIdParamValidation,
|
||||||
getMonitorByIdQueryValidation,
|
getMonitorByIdQueryValidation,
|
||||||
getMonitorsByTeamIdParamValidation,
|
|
||||||
getMonitorsByTeamIdQueryValidation,
|
getMonitorsByTeamIdQueryValidation,
|
||||||
getMonitorsWithChecksQueryValidation,
|
getMonitorsWithChecksQueryValidation,
|
||||||
createMonitorBodyValidation,
|
createMonitorBodyValidation,
|
||||||
@@ -12,22 +11,14 @@ 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 {
|
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "./controllerUtils.js";
|
||||||
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 } from "@/service/index.js";
|
import { IMonitorService } from "@/service/index.js";
|
||||||
import { GeoContinent } from "@/types/geoCheck.js";
|
|
||||||
|
|
||||||
const SERVICE_NAME = "monitorController";
|
const SERVICE_NAME = "monitorController";
|
||||||
class MonitorController {
|
class MonitorController {
|
||||||
@@ -45,10 +36,9 @@ class MonitorController {
|
|||||||
|
|
||||||
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
|
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getCertificateParamValidation.parse(req.params);
|
const validatedParams = getCertificateParamValidation.parse(req.params);
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const monitorId = requireString(req.params?.monitorId, "Monitor ID");
|
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId });
|
||||||
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({
|
||||||
@@ -65,10 +55,14 @@ class MonitorController {
|
|||||||
|
|
||||||
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params);
|
||||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query);
|
||||||
const normalize = optionalBoolean(req?.query?.normalize, "normalize");
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const monitorId = validatedParams.monitorId;
|
||||||
|
const dateRange = validatedQuery.dateRange;
|
||||||
|
const normalize = validatedQuery.normalize;
|
||||||
|
|
||||||
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const data = await this.monitorService.getUptimeDetailsById({
|
const data = await this.monitorService.getUptimeDetailsById({
|
||||||
teamId,
|
teamId,
|
||||||
@@ -88,12 +82,12 @@ class MonitorController {
|
|||||||
|
|
||||||
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getHardwareDetailsByIdParamValidation.parse(req.params);
|
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||||
getHardwareDetailsByIdQueryValidation.parse(req.query);
|
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = validatedParams.monitorId;
|
||||||
const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent";
|
const dateRange = validatedQuery.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,
|
||||||
@@ -112,12 +106,12 @@ class MonitorController {
|
|||||||
};
|
};
|
||||||
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getHardwareDetailsByIdParamValidation.parse(req.params);
|
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||||
getHardwareDetailsByIdQueryValidation.parse(req.query);
|
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = validatedParams.monitorId;
|
||||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
const dateRange = validatedQuery.dateRange || "recent";
|
||||||
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,
|
||||||
@@ -137,18 +131,14 @@ class MonitorController {
|
|||||||
|
|
||||||
getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getMonitorByIdParamValidation.parse(req.params);
|
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||||
getMonitorByIdQueryValidation.parse(req.query);
|
const validatedQuery = getMonitorByIdQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = validatedParams.monitorId;
|
||||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
const dateRange = validatedQuery.dateRange || "recent";
|
||||||
const continentParam = req?.query?.continent;
|
const continentParam = validatedQuery.continent;
|
||||||
const continents = continentParam
|
const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined;
|
||||||
? Array.isArray(continentParam)
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
? (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,
|
||||||
@@ -169,11 +159,10 @@ class MonitorController {
|
|||||||
|
|
||||||
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
|
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getMonitorByIdParamValidation.parse(req.params);
|
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||||
getMonitorByIdQueryValidation.parse(req.query);
|
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = validatedParams.monitorId;
|
||||||
|
|
||||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||||
|
|
||||||
@@ -189,12 +178,12 @@ class MonitorController {
|
|||||||
|
|
||||||
createMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
createMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
createMonitorBodyValidation.parse(req.body);
|
const validatedBody = createMonitorBodyValidation.parse(req.body);
|
||||||
|
|
||||||
const userId = requireString(req?.user?.id, "User ID");
|
const userId = requireUserId(req.user?.id);
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
|
const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -208,14 +197,10 @@ class MonitorController {
|
|||||||
|
|
||||||
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 } = req.body;
|
const monitors = validatedBody.monitors;
|
||||||
|
|
||||||
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 });
|
||||||
|
|
||||||
@@ -231,9 +216,9 @@ class MonitorController {
|
|||||||
|
|
||||||
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getMonitorByIdParamValidation.parse(req.params);
|
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const monitorId = validatedParams.monitorId;
|
||||||
|
|
||||||
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
|
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
|
||||||
|
|
||||||
@@ -249,8 +234,7 @@ class MonitorController {
|
|||||||
|
|
||||||
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({
|
||||||
@@ -264,12 +248,12 @@ class MonitorController {
|
|||||||
|
|
||||||
editMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
editMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getMonitorByIdParamValidation.parse(req.params);
|
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||||
editMonitorBodyValidation.parse(req.body);
|
const validatedBody = editMonitorBodyValidation.parse(req.body);
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = validatedParams.monitorId;
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
|
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -283,10 +267,10 @@ class MonitorController {
|
|||||||
|
|
||||||
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
pauseMonitorParamValidation.parse(req.params);
|
const validatedParams = pauseMonitorParamValidation.parse(req.params);
|
||||||
|
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = validatedParams.monitorId;
|
||||||
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 });
|
||||||
|
|
||||||
@@ -302,8 +286,8 @@ class MonitorController {
|
|||||||
|
|
||||||
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const id = requireString(req?.user?.id, "User ID");
|
const id = requireUserId(req.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({
|
||||||
@@ -316,32 +300,13 @@ class MonitorController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
||||||
getMonitorsByTeamIdParamValidation.parse(req.params);
|
const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query);
|
||||||
getMonitorsByTeamIdQueryValidation.parse(req.query);
|
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const type = parseMonitorTypeFilter(req.query?.type);
|
const type = validatedQuery.type;
|
||||||
const filter = optionalString(req.query?.filter, "filter");
|
const filter = validatedQuery.filter;
|
||||||
|
|
||||||
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
|
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
|
||||||
|
|
||||||
@@ -357,17 +322,17 @@ class MonitorController {
|
|||||||
|
|
||||||
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getMonitorsByTeamIdParamValidation.parse(req.params);
|
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query);
|
||||||
getMonitorsWithChecksQueryValidation.parse(req.query);
|
|
||||||
const explain = optionalBoolean(req?.query?.explain, "explain");
|
const explain = validatedQuery.explain;
|
||||||
const limit = optionalNumber(req?.query?.limit, "limit");
|
const limit = validatedQuery.limit;
|
||||||
const page = optionalNumber(req?.query?.page, "page");
|
const page = validatedQuery.page;
|
||||||
const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage");
|
const rowsPerPage = validatedQuery.rowsPerPage;
|
||||||
const filter = optionalString(req?.query?.filter, "filter");
|
const filter = validatedQuery.filter;
|
||||||
const field = optionalString(req?.query?.field, "field");
|
const field = validatedQuery.field;
|
||||||
const order = parseSortOrder(req?.query?.order);
|
const order = validatedQuery.order;
|
||||||
const type = parseMonitorTypeFilter(req?.query?.type);
|
const type = validatedQuery.type;
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
|
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
|
||||||
teamId,
|
teamId,
|
||||||
@@ -392,11 +357,7 @@ class MonitorController {
|
|||||||
|
|
||||||
exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => {
|
exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = req?.user?.teamId;
|
const teamId = requireTeamId(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({
|
||||||
@@ -424,11 +385,7 @@ class MonitorController {
|
|||||||
|
|
||||||
getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = req?.user?.teamId;
|
const teamId = requireTeamId(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({
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
import { Notification } from "@/types/index.js";
|
import {
|
||||||
import { createNotificationBodyValidation } from "@/validation/notificationValidation.js";
|
createNotificationBodyValidation,
|
||||||
|
deleteNotificationParamValidation,
|
||||||
|
getNotificationByIdParamValidation,
|
||||||
|
testNotificationBodyValidation,
|
||||||
|
editNotificationParamValidation,
|
||||||
|
testAllNotificationsBodyValidation,
|
||||||
|
} from "@/validation/notificationValidation.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||||
import { INotificationsService } from "@/service/index.js";
|
import { INotificationsService } from "@/service/index.js";
|
||||||
import { requireTeamId } from "./controllerUtils.js";
|
import { requireTeamId, requireUserId } from "./controllerUtils.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "NotificationController";
|
const SERVICE_NAME = "NotificationController";
|
||||||
|
|
||||||
class NotificationController {
|
class NotificationController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
|
||||||
private notificationsService: INotificationsService;
|
private notificationsService: INotificationsService;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
|
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
|
||||||
@@ -18,13 +23,9 @@ class NotificationController {
|
|||||||
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: Notification = req.body;
|
const notification = testNotificationBodyValidation.parse(req.body);
|
||||||
const success = await this.notificationsService.sendTestNotification(notification);
|
const success = await this.notificationsService.sendTestNotification(notification);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@@ -34,6 +35,7 @@ class NotificationController {
|
|||||||
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);
|
||||||
@@ -42,23 +44,12 @@ class NotificationController {
|
|||||||
|
|
||||||
createNotification = async (req: Request, res: Response, next: NextFunction) => {
|
createNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
createNotificationBodyValidation.parse(req.body);
|
const validatedBody = createNotificationBodyValidation.parse(req.body);
|
||||||
|
|
||||||
const body = req.body;
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
const userId = requireUserId(req.user?.id);
|
||||||
|
|
||||||
const teamId = req?.user?.teamId;
|
const notification = await this.notificationsService.createNotification(validatedBody, userId, 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,11 +62,7 @@ class NotificationController {
|
|||||||
|
|
||||||
getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = req?.user?.teamId;
|
const teamId = requireTeamId(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({
|
||||||
@@ -90,17 +77,10 @@ class NotificationController {
|
|||||||
|
|
||||||
deleteNotification = async (req: Request, res: Response, next: NextFunction) => {
|
deleteNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = req?.user?.teamId;
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
if (!teamId) {
|
const validatedParams = deleteNotificationParamValidation.parse(req.params);
|
||||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const notificationId = req.params.id as string;
|
await this.notificationsService.deleteById(validatedParams.id, teamId);
|
||||||
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",
|
||||||
@@ -113,12 +93,9 @@ class NotificationController {
|
|||||||
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 notificationId = req.params.id as string;
|
const validatedParams = getNotificationByIdParamValidation.parse(req.params);
|
||||||
if (!notificationId) {
|
|
||||||
throw new AppError({ message: "Notification ID is required", status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const notification = await this.notificationsService.findById(notificationId, teamId);
|
const notification = await this.notificationsService.findById(validatedParams.id, teamId);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -132,14 +109,13 @@ class NotificationController {
|
|||||||
|
|
||||||
editNotification = async (req: Request, res: Response, next: NextFunction) => {
|
editNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
createNotificationBodyValidation.parse(req.body);
|
const validatedBody = createNotificationBodyValidation.parse(req.body);
|
||||||
|
const validatedParams = editNotificationParamValidation.parse(req.params);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const notificationId = req.params.id as string;
|
const notificationId = validatedParams.id;
|
||||||
if (!notificationId) {
|
|
||||||
throw new AppError({ message: "Notification ID is required", status: 400 });
|
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody);
|
||||||
}
|
|
||||||
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",
|
||||||
@@ -152,13 +128,11 @@ class NotificationController {
|
|||||||
|
|
||||||
testAllNotifications = async (req: Request, res: Response, next: NextFunction) => {
|
testAllNotifications = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const monitorId = req.body.monitorId;
|
const validatedBody = testAllNotificationsBodyValidation.parse(req.body);
|
||||||
const teamId = req?.user?.teamId;
|
|
||||||
if (!teamId) {
|
|
||||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
const teamId = requireTeamId(req.user?.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) {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IDb {
|
||||||
|
connect(): Promise<void>;
|
||||||
|
disconnect(): Promise<void>;
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
import AppSettings from "./models/AppSettings.js";
|
import AppSettings from "@/db/models/AppSettings.js";
|
||||||
import { runMigrations } from "./migration/index.js";
|
import { runMigrations } from "@/db/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";
|
||||||
class MongoDB {
|
import { IDb } from "@/db/IDb.js";
|
||||||
|
|
||||||
|
class MongoDB implements IDb {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -46,12 +48,12 @@ class MongoDB {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "connect",
|
method: "connect",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -63,12 +65,12 @@ class MongoDB {
|
|||||||
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: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "disconnect",
|
method: "disconnect",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { MonitorModel } from "../models/Monitor.js";
|
||||||
|
import { logger } from "@/utils/logger.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix infrastructure monitors that had thresholds incorrectly set to 0 or 5
|
||||||
|
* due to the old default value. Updates them to 100 (disabled).
|
||||||
|
*/
|
||||||
|
export async function fixInfrastructureThresholds(): Promise<void> {
|
||||||
|
const SERVICE_NAME = "Migration:FixInfrastructureThresholds";
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info({ service: SERVICE_NAME, message: "Starting infrastructure threshold fix" });
|
||||||
|
|
||||||
|
const thresholdFields = ["cpuAlertThreshold", "memoryAlertThreshold", "diskAlertThreshold", "tempAlertThreshold"];
|
||||||
|
|
||||||
|
// Find hardware monitors with any threshold set to 0 or 5
|
||||||
|
const filter = {
|
||||||
|
type: "hardware",
|
||||||
|
$or: thresholdFields.flatMap((field) => [{ [field]: 0 }, { [field]: 5 }]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const monitors = await MonitorModel.find(filter);
|
||||||
|
|
||||||
|
if (monitors.length === 0) {
|
||||||
|
logger.info({ service: SERVICE_NAME, message: "No monitors with threshold value of 0 or 5 found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
message: `Found ${monitors.length} monitors with threshold value of 0 or 5`,
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedCount = 0;
|
||||||
|
|
||||||
|
for (const monitor of monitors) {
|
||||||
|
const update: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const field of thresholdFields) {
|
||||||
|
if ((monitor as any)[field] === 0 || (monitor as any)[field] === 5) {
|
||||||
|
update[field] = 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(update).length > 0) {
|
||||||
|
await MonitorModel.updateOne({ _id: monitor._id }, { $set: update });
|
||||||
|
updatedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
message: `Fixed ${updatedCount} monitors — thresholds updated from 0/5 to 100`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.error({ service: SERVICE_NAME, message: `Error fixing thresholds: ${errorMessage}` });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +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";
|
||||||
|
|
||||||
type MigrationEntry = {
|
type MigrationEntry = {
|
||||||
@@ -12,6 +13,7 @@ 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 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
||||||
|
|||||||
@@ -21,13 +21,10 @@ type CheckMetadataDocument = Omit<CheckMetadata, "monitorId" | "teamId"> & {
|
|||||||
type: MonitorType;
|
type: MonitorType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CheckDocumentBase = Omit<Check, "id" | "metadata" | "expiry" | "ackAt" | "createdAt" | "updatedAt"> & {
|
type CheckDocumentBase = Omit<Check, "id" | "metadata" | "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 {
|
||||||
@@ -225,18 +222,7 @@ 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: () => ({}),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
|
|||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[],
|
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as NotificationChannel[],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
notificationName: {
|
notificationName: {
|
||||||
|
|||||||
+3
-4
@@ -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/system/settingsService.js";
|
import { SettingsService } from "@/service/index.js";
|
||||||
import { MongoSettingsRepository } from "./repositories/index.js";
|
import { MongoSettingsRepository } from "./repositories/index.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "Server";
|
const SERVICE_NAME = "Server";
|
||||||
@@ -50,9 +50,8 @@ const startApp = async () => {
|
|||||||
openApiSpec,
|
openApiSpec,
|
||||||
});
|
});
|
||||||
|
|
||||||
const port = 52345;
|
const server = app.listen(env.PORT, () => {
|
||||||
const server = app.listen(port, () => {
|
logger.info({ message: `Server started on port:${env.PORT}` });
|
||||||
logger.info({ message: `Server started on port:${port}` });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
initShutdownListener(server, services);
|
initShutdownListener(server, services);
|
||||||
|
|||||||
@@ -20,12 +20,19 @@ export interface IChecksRepository {
|
|||||||
monitorId: string,
|
monitorId: string,
|
||||||
sortOrder: string,
|
sortOrder: string,
|
||||||
dateRange: string,
|
dateRange: string,
|
||||||
filter: string,
|
filter: string | undefined,
|
||||||
page: number,
|
page: number,
|
||||||
rowsPerPage: number,
|
rowsPerPage: number,
|
||||||
status: boolean | undefined
|
status: boolean | undefined
|
||||||
): Promise<ChecksQueryResult>;
|
): Promise<ChecksQueryResult>;
|
||||||
findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise<ChecksQueryResult>;
|
findByTeamId(
|
||||||
|
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,
|
||||||
@@ -40,4 +47,5 @@ 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,9 +13,10 @@ import type {
|
|||||||
GotTimings,
|
GotTimings,
|
||||||
MonitorType,
|
MonitorType,
|
||||||
} from "@/types/index.js";
|
} from "@/types/index.js";
|
||||||
import { CheckModel, MonitorModel, type CheckDocument } from "@/db/models/index.js";
|
import { CheckModel, 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";
|
||||||
|
|
||||||
@@ -26,8 +27,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: any;
|
private logger: ILogger;
|
||||||
constructor(logger: any) {
|
constructor(logger: ILogger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,13 +47,6 @@ 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,
|
||||||
@@ -171,9 +165,6 @@ 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),
|
||||||
@@ -186,7 +177,6 @@ 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),
|
||||||
};
|
};
|
||||||
@@ -231,13 +221,13 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
monitorId: string,
|
monitorId: string,
|
||||||
sortOrder: string,
|
sortOrder: string,
|
||||||
dateRange: string,
|
dateRange: string,
|
||||||
filter: string,
|
filter: string | undefined,
|
||||||
page: number,
|
page: number,
|
||||||
rowsPerPage: number,
|
rowsPerPage: number,
|
||||||
status: boolean | undefined
|
status: boolean | undefined
|
||||||
) => {
|
) => {
|
||||||
// Match
|
// Match
|
||||||
const matchStage: Record<string, any> = {
|
const matchStage: Record<string, unknown> = {
|
||||||
"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) && {
|
||||||
@@ -289,7 +279,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, any> = {
|
const matchStage: Record<string, unknown> = {
|
||||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||||
...(getDateForRange(dateRange) && {
|
...(getDateForRange(dateRange) && {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
@@ -358,7 +348,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: any) => this.toEntity(doc as CheckDocument));
|
acc[monitorId] = docs.map((doc: CheckDocument) => this.toEntity(doc));
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
return mapped;
|
return mapped;
|
||||||
@@ -412,6 +402,31 @@ 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,17 +1,18 @@
|
|||||||
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 { GeoChecksQueryResult, FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
|
import type { FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
|
||||||
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
|
import { GeoCheckModel, type GeoCheckDocument } 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 = "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: any;
|
private logger: ILogger;
|
||||||
constructor(logger: any) {
|
constructor(logger: ILogger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise<MaintenanceWindow[]>;
|
findByTeamId(teamId: string, page: number, rowsPerPage: number, field?: string, order?: string, active?: boolean): 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>;
|
||||||
}
|
}
|
||||||
@@ -75,11 +75,11 @@ 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, any> = { teamId };
|
const maintenanceQuery: Record<string, any> = { teamId };
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ 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, any> = { teamId };
|
const maintenanceQuery: Record<string, any> = { teamId };
|
||||||
|
|
||||||
if (active !== undefined) maintenanceQuery.active = active;
|
if (active !== undefined) maintenanceQuery.active = active;
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ 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(["superadmin"]), this.authController.getUserById);
|
this.router.get("/users/:userId", verifyJWT, isAllowed(["admin", "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);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ 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);
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ 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
|
||||||
|
|||||||
@@ -1,25 +1,48 @@
|
|||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
|
import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||||
import type { MonitorStatusResponse, CheckErrorInfo, Check, ILighthouseAudit, ChecksQueryResult, ChecksSummary } from "@/types/index.js";
|
import type {
|
||||||
|
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<PageSpeedStatusPayload | HardwareStatusPayload | undefined>): Partial<Check> | undefined;
|
buildCheck(statusResponse: MonitorStatusResponse<MonitorPayloadMap[keyof MonitorPayloadMap]>): Check | undefined;
|
||||||
getChecksByMonitor(params: { monitorId: string; query: Record<string, string>; teamId: string }): Promise<ChecksQueryResult>;
|
getChecksByMonitor(params: {
|
||||||
getChecksByTeam(params: { teamId: string; query: Record<string, string> }): Promise<ChecksQueryResult>;
|
monitorId: string;
|
||||||
|
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>;
|
||||||
updateChecksTTL(params: { teamId: string; ttl: string }): Promise<void>;
|
deleteOlderThan(date: Date): Promise<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CheckService implements ICheckService {
|
export class CheckService implements ICheckService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
@@ -39,12 +62,14 @@ class CheckService implements ICheckService {
|
|||||||
return this.checksRepository.createChecks(checks);
|
return this.checksRepository.createChecks(checks);
|
||||||
};
|
};
|
||||||
|
|
||||||
buildCheck = (statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>) => {
|
buildCheck = (statusResponse: MonitorStatusResponse<MonitorPayloadMap[keyof MonitorPayloadMap]>): Check | undefined => {
|
||||||
const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse;
|
const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse;
|
||||||
|
|
||||||
const check: Partial<Check> = {
|
const now = new Date().toISOString();
|
||||||
|
const check: Check = {
|
||||||
id: new Types.ObjectId().toString(),
|
id: new Types.ObjectId().toString(),
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
metadata: {
|
metadata: {
|
||||||
monitorId,
|
monitorId,
|
||||||
teamId,
|
teamId,
|
||||||
@@ -112,7 +137,25 @@ class CheckService implements ICheckService {
|
|||||||
return check;
|
return check;
|
||||||
};
|
};
|
||||||
|
|
||||||
getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: Record<string, string>; teamId: string }) => {
|
getChecksByMonitor = async ({
|
||||||
|
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 });
|
||||||
}
|
}
|
||||||
@@ -123,71 +166,54 @@ 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 { sortOrder, dateRange, filter, page, rowsPerPage, status } = query;
|
const parsedPage = page ?? 0;
|
||||||
if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) {
|
const parsedRowsPerPage = rowsPerPage ?? 5;
|
||||||
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, parsedStatus);
|
const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, status);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
getChecksByTeam = async ({ teamId, query }: { teamId: string; query: Record<string, string> }) => {
|
getChecksByTeam = async ({
|
||||||
const { sortOrder, dateRange, filter, page, rowsPerPage } = query;
|
teamId,
|
||||||
if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) {
|
sortOrder,
|
||||||
throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
|
dateRange,
|
||||||
}
|
filter,
|
||||||
|
page,
|
||||||
if (!teamId) {
|
rowsPerPage,
|
||||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
|
}: {
|
||||||
}
|
teamId: string;
|
||||||
|
sortOrder: string;
|
||||||
const parsedPage = page ? parseInt(page) : 0;
|
dateRange: string;
|
||||||
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5;
|
page: number;
|
||||||
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => {
|
deleteOlderThan = async (date: Date) => {
|
||||||
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } });
|
const deletedCount = await this.checksRepository.deleteOlderThan(date);
|
||||||
|
return deletedCount;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CheckService;
|
|
||||||
@@ -3,15 +3,19 @@ import os from "os";
|
|||||||
|
|
||||||
const SERVICE_NAME = "diagnosticService";
|
const SERVICE_NAME = "diagnosticService";
|
||||||
|
|
||||||
interface MemoryUsageMetrics {
|
export interface IDiagnosticService {
|
||||||
rss: number;
|
getCPUUsage(): Promise<{ userUsageMs: number; systemUsageMs: number; usagePercentage: number }>;
|
||||||
heapTotal: number;
|
getSystemStats(): Promise<{
|
||||||
heapUsed: number;
|
osStats: { freeMemoryBytes: number; totalMemoryBytes: number };
|
||||||
external: number;
|
memoryUsage: Record<keyof NodeJS.MemoryUsage, number>;
|
||||||
arrayBuffers: number;
|
cpuUsage: { userUsageMs: number; systemUsageMs: number; usagePercentage: number };
|
||||||
|
v8HeapStats: { totalHeapSizeBytes: number; usedHeapSizeBytes: number; heapSizeLimitBytes: number };
|
||||||
|
eventLoopDelayMs: number;
|
||||||
|
uptimeMs: number;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DiagnosticService {
|
export class DiagnosticService implements IDiagnosticService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -103,5 +107,3 @@ class DiagnosticService {
|
|||||||
return diagnostics;
|
return diagnostics;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DiagnosticService;
|
|
||||||
@@ -16,7 +16,7 @@ export interface IGeoChecksService {
|
|||||||
getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise<any>;
|
getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeoChecksService implements IGeoChecksService {
|
export class GeoChecksService implements IGeoChecksService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -68,7 +68,7 @@ class GeoChecksService implements IGeoChecksService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Create measurement request
|
// Step 1: Create measurement request
|
||||||
const measurementId = await this.globalPingService.createMeasurement(monitor.url, monitor.geoCheckLocations);
|
const measurementId = await this.globalPingService.createMeasurement(monitor.type, monitor.url, monitor.geoCheckLocations);
|
||||||
|
|
||||||
if (!measurementId) {
|
if (!measurementId) {
|
||||||
// GlobalPing API is down, skip this check
|
// GlobalPing API is down, skip this check
|
||||||
@@ -76,7 +76,6 @@ class GeoChecksService implements IGeoChecksService {
|
|||||||
message: "Skipping geo check due to API unavailability",
|
message: "Skipping geo check due to API unavailability",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "buildGeoCheck",
|
method: "buildGeoCheck",
|
||||||
details: { monitorId: monitor.id },
|
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -90,7 +89,6 @@ class GeoChecksService implements IGeoChecksService {
|
|||||||
message: "No successful geo check results",
|
message: "No successful geo check results",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "buildGeoCheck",
|
method: "buildGeoCheck",
|
||||||
details: { monitorId: monitor.id, measurementId },
|
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -102,17 +100,16 @@ class GeoChecksService implements IGeoChecksService {
|
|||||||
message: `Geo check completed for monitor ${monitor.id}`,
|
message: `Geo check completed for monitor ${monitor.id}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "buildGeoCheck",
|
method: "buildGeoCheck",
|
||||||
details: { monitorId: monitor.id, resultsCount: results.length },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return geoCheck;
|
return geoCheck;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: "Error executing geo check",
|
message: "Error executing geo check",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "buildGeoCheck",
|
method: "buildGeoCheck",
|
||||||
details: { monitorId: monitor.id, error: error.message },
|
details: { monitorId: monitor.id, error: error instanceof Error ? error.message : "Unknown error" },
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -172,21 +169,9 @@ class GeoChecksService implements IGeoChecksService {
|
|||||||
|
|
||||||
const { sortOrder, dateRange, page, rowsPerPage, continent } = query;
|
const { sortOrder, dateRange, page, rowsPerPage, continent } = query;
|
||||||
const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined;
|
const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined;
|
||||||
|
|
||||||
this.logger.debug({
|
|
||||||
message: "getGeoChecksByMonitor query params",
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
method: "getGeoChecksByMonitor",
|
|
||||||
details: { continent, continents, query },
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsedPage = page ? parseInt(page) : page;
|
const parsedPage = page ? parseInt(page) : page;
|
||||||
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
|
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
|
||||||
|
|
||||||
const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents);
|
const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GeoChecksService;
|
|
||||||
@@ -2,15 +2,36 @@ const SERVICE_NAME = "incidentService";
|
|||||||
import type { Monitor } from "@/types/monitor.js";
|
import type { Monitor } from "@/types/monitor.js";
|
||||||
import type { MonitorStatusResponse } from "@/types/network.js";
|
import type { MonitorStatusResponse } from "@/types/network.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { ParseBoolean } from "@/utils/utils.js";
|
|
||||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||||
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
||||||
import type { Incident } from "@/types/index.js";
|
import type { Incident, IncidentSummary, User } from "@/types/index.js";
|
||||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||||
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
||||||
import type { ILogger } from "@/utils/logger.js";
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
class IncidentService {
|
export interface IIncidentService {
|
||||||
|
handleIncident(
|
||||||
|
monitor: Monitor,
|
||||||
|
code: number,
|
||||||
|
decision: MonitorActionDecision,
|
||||||
|
monitorStatusResponse?: MonitorStatusResponse
|
||||||
|
): Promise<Incident | null>;
|
||||||
|
resolveIncident(incidentId: string, userId: string, teamId: string, comment?: string, userEmail?: string): Promise<Incident>;
|
||||||
|
getIncidentsByTeam(
|
||||||
|
teamId: string,
|
||||||
|
sortOrder: string,
|
||||||
|
dateRange: string,
|
||||||
|
page: number,
|
||||||
|
rowsPerPage: number,
|
||||||
|
status: boolean | undefined,
|
||||||
|
monitorId: string | undefined,
|
||||||
|
resolutionType: string | undefined
|
||||||
|
): Promise<{ incidents: Incident[]; count: number }>;
|
||||||
|
getIncidentSummary(teamId: string, limit?: number): Promise<IncidentSummary>;
|
||||||
|
getIncidentById(incidentId: string, teamId: string): Promise<{ incident: Incident; monitor: Monitor; user: User | null }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IncidentService implements IIncidentService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -142,13 +163,13 @@ class IncidentService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return resolvedIncident;
|
return resolvedIncident;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "resolveIncident",
|
method: "resolveIncident",
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
details: { id: incidentId },
|
details: { id: incidentId },
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -158,11 +179,11 @@ class IncidentService {
|
|||||||
teamId: string,
|
teamId: string,
|
||||||
sortOrder: string,
|
sortOrder: string,
|
||||||
dateRange: string,
|
dateRange: string,
|
||||||
page: string,
|
page: number,
|
||||||
rowsPerPage: string,
|
rowsPerPage: number,
|
||||||
status: string,
|
status: boolean | undefined,
|
||||||
monitorId: string,
|
monitorId: string | undefined,
|
||||||
resolutionType: string
|
resolutionType: string | undefined
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
if (!teamId) {
|
if (!teamId) {
|
||||||
@@ -171,9 +192,8 @@ class IncidentService {
|
|||||||
|
|
||||||
const startDate = getDateForRange(dateRange);
|
const startDate = getDateForRange(dateRange);
|
||||||
|
|
||||||
const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
|
const parsedPage = page ?? 0;
|
||||||
const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20;
|
const parsedRowsPerPage = rowsPerPage ?? 20;
|
||||||
const parsedStatus = typeof status === "undefined" ? undefined : ParseBoolean(status);
|
|
||||||
|
|
||||||
const incidents = await this.incidentsRepository.findByTeamId(
|
const incidents = await this.incidentsRepository.findByTeamId(
|
||||||
teamId,
|
teamId,
|
||||||
@@ -181,43 +201,43 @@ class IncidentService {
|
|||||||
parsedPage,
|
parsedPage,
|
||||||
parsedRowsPerPage,
|
parsedRowsPerPage,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
parsedStatus,
|
status,
|
||||||
monitorId,
|
monitorId,
|
||||||
resolutionType
|
resolutionType
|
||||||
);
|
);
|
||||||
|
|
||||||
const count = await this.incidentsRepository.countByTeamId(teamId, startDate, parsedStatus, monitorId, resolutionType);
|
const count = await this.incidentsRepository.countByTeamId(teamId, startDate, status, monitorId, resolutionType);
|
||||||
|
|
||||||
return { incidents, count };
|
return { incidents, count };
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getIncidentsByTeam",
|
method: "getIncidentsByTeam",
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
details: { teamId },
|
details: { teamId },
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getIncidentSummary = async (teamId: string, limit?: string) => {
|
getIncidentSummary = async (teamId: string, limit?: number) => {
|
||||||
try {
|
try {
|
||||||
if (!teamId) {
|
if (!teamId) {
|
||||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentSummary", status: 400 });
|
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentSummary", status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedLimit = limit && Number.isFinite(parseInt(limit, 10)) ? parseInt(limit, 10) : 10;
|
const parsedLimit = limit ?? 10;
|
||||||
const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit);
|
const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit);
|
||||||
|
|
||||||
return summary;
|
return summary;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getIncidentSummary",
|
method: "getIncidentSummary",
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
details: { teamId },
|
details: { teamId },
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -232,17 +252,15 @@ class IncidentService {
|
|||||||
user = await this.usersRepository.findById(incident.resolvedBy);
|
user = await this.usersRepository.findById(incident.resolvedBy);
|
||||||
}
|
}
|
||||||
return { incident, monitor, user };
|
return { incident, monitor, user };
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getIncidentById",
|
method: "getIncidentById",
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
details: { incidentId },
|
details: { incidentId },
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IncidentService;
|
|
||||||
@@ -5,7 +5,13 @@ import { AppError } from "@/utils/AppError.js";
|
|||||||
|
|
||||||
const SERVICE_NAME = "inviteService";
|
const SERVICE_NAME = "inviteService";
|
||||||
|
|
||||||
class InviteService {
|
export interface IInviteService {
|
||||||
|
getInviteToken(params: { invite: Partial<Invite>; teamId: string; userRoles: UserRole[] }): Promise<Invite>;
|
||||||
|
sendInviteEmail(params: { invite: Partial<Invite>; firstName: string; userRoles: UserRole[] }): Promise<void>;
|
||||||
|
verifyInviteToken(params: { inviteToken: string }): Promise<Invite>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InviteService implements IInviteService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private settingsService: any;
|
private settingsService: any;
|
||||||
@@ -88,5 +94,3 @@ class InviteService {
|
|||||||
return await this.invitesRepository.findByToken(inviteToken);
|
return await this.invitesRepository.findByToken(inviteToken);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InviteService;
|
|
||||||
@@ -1,10 +1,26 @@
|
|||||||
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
|
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||||
import { ParseBoolean } from "@/utils/utils.js";
|
import type { MaintenanceWindow } from "@/types/index.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "maintenanceWindowService";
|
const SERVICE_NAME = "maintenanceWindowService";
|
||||||
|
|
||||||
class MaintenanceWindowService {
|
export interface IMaintenanceWindowService {
|
||||||
|
createMaintenanceWindow(params: { teamId: string; body: Partial<MaintenanceWindow> & { monitors: string[] } }): Promise<void>;
|
||||||
|
getMaintenanceWindowById(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
|
||||||
|
getMaintenanceWindowsByTeamId(params: {
|
||||||
|
teamId: string;
|
||||||
|
active?: boolean;
|
||||||
|
page?: number;
|
||||||
|
rowsPerPage?: number;
|
||||||
|
field?: string;
|
||||||
|
order?: string;
|
||||||
|
}): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>;
|
||||||
|
getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise<MaintenanceWindow[]>;
|
||||||
|
deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
|
||||||
|
editMaintenanceWindow(params: { id: string; teamId: string; body: Partial<MaintenanceWindow> }): Promise<MaintenanceWindow>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MaintenanceWindowService implements IMaintenanceWindowService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||||
@@ -59,14 +75,25 @@ class MaintenanceWindowService {
|
|||||||
return await this.maintenanceWindowsRepository.findById(id, teamId);
|
return await this.maintenanceWindowsRepository.findById(id, teamId);
|
||||||
};
|
};
|
||||||
|
|
||||||
getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => {
|
getMaintenanceWindowsByTeamId = async ({
|
||||||
let { active, page, rowsPerPage, field, order } = query || {};
|
teamId,
|
||||||
|
active,
|
||||||
|
page,
|
||||||
|
rowsPerPage,
|
||||||
|
field,
|
||||||
|
order,
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
active?: boolean;
|
||||||
|
page?: number;
|
||||||
|
rowsPerPage?: number;
|
||||||
|
field?: string;
|
||||||
|
order?: string;
|
||||||
|
}) => {
|
||||||
|
page = page ?? 0;
|
||||||
|
rowsPerPage = rowsPerPage ?? 10;
|
||||||
|
|
||||||
active = typeof active === "undefined" ? undefined : ParseBoolean(active);
|
const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, page, rowsPerPage, field, order, active);
|
||||||
page = parseInt(page) || 0;
|
|
||||||
rowsPerPage = parseInt(rowsPerPage) || 10;
|
|
||||||
|
|
||||||
const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, active, page, rowsPerPage, field, order);
|
|
||||||
const maintenanceWindowCount = await this.maintenanceWindowsRepository.countByTeamId(teamId, active);
|
const maintenanceWindowCount = await this.maintenanceWindowsRepository.countByTeamId(teamId, active);
|
||||||
return { maintenanceWindows, maintenanceWindowCount };
|
return { maintenanceWindows, maintenanceWindowCount };
|
||||||
};
|
};
|
||||||
@@ -83,5 +110,3 @@ class MaintenanceWindowService {
|
|||||||
return await this.maintenanceWindowsRepository.updateById(id, teamId, body);
|
return await this.maintenanceWindowsRepository.updateById(id, teamId, body);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MaintenanceWindowService;
|
|
||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
PageSpeedDetailsResult,
|
PageSpeedDetailsResult,
|
||||||
GamesMap,
|
GamesMap,
|
||||||
} from "@/types/monitor.js";
|
} from "@/types/monitor.js";
|
||||||
|
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||||
import type { GeoContinent } from "@/types/geoCheck.js";
|
import type { GeoContinent } from "@/types/geoCheck.js";
|
||||||
import type {
|
import type {
|
||||||
IChecksRepository,
|
IChecksRepository,
|
||||||
@@ -17,9 +18,7 @@ import type {
|
|||||||
IMonitorStatsRepository,
|
IMonitorStatsRepository,
|
||||||
IStatusPagesRepository,
|
IStatusPagesRepository,
|
||||||
} from "@/repositories/index.js";
|
} from "@/repositories/index.js";
|
||||||
import fs from "fs";
|
import demoMonitorsData from "@/utils/demoMonitors.json" with { type: "json" };
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
import path from "path";
|
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ export interface IMonitorService {
|
|||||||
readonly serviceName: string;
|
readonly serviceName: string;
|
||||||
|
|
||||||
// create
|
// create
|
||||||
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
|
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>;
|
||||||
createMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
|
createMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
|
||||||
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
|
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
|
||||||
|
|
||||||
@@ -65,7 +64,7 @@ export interface IMonitorService {
|
|||||||
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
|
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
|
||||||
|
|
||||||
// update
|
// update
|
||||||
editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise<Monitor>;
|
editMonitor(args: { teamId: string; monitorId: string; body: Partial<Monitor> }): Promise<Monitor>;
|
||||||
pauseMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
pauseMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
||||||
|
|
||||||
// delete
|
// delete
|
||||||
@@ -73,7 +72,6 @@ export interface IMonitorService {
|
|||||||
deleteAllMonitors(args: { teamId: string }): Promise<number>;
|
deleteAllMonitors(args: { teamId: string }): Promise<number>;
|
||||||
|
|
||||||
// other
|
// other
|
||||||
sendTestEmail(args: { to: string }): Promise<string>;
|
|
||||||
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
|
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
|
||||||
importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): Promise<{ imported: number; errors: string[] }>;
|
importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): Promise<{ imported: number; errors: string[] }>;
|
||||||
}
|
}
|
||||||
@@ -176,23 +174,17 @@ export class MonitorService implements IMonitorService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<any[]> => {
|
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<any[]> => {
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const demoData = demoMonitorsData;
|
||||||
const __dirname = path.dirname(__filename);
|
const monitors = demoData.map((monitor) => ({
|
||||||
const demoMonitorsPath = path.resolve(__dirname, "../../utils/demoMonitors.json");
|
userId,
|
||||||
|
teamId,
|
||||||
const demoData = JSON.parse(fs.readFileSync(demoMonitorsPath, "utf8"));
|
name: monitor.name,
|
||||||
const monitors: Monitor[] = demoData.map((monitor: Monitor) => {
|
description: monitor.name,
|
||||||
return {
|
type: "http" as const,
|
||||||
userId,
|
url: monitor.url,
|
||||||
teamId,
|
interval: 60000,
|
||||||
name: monitor.name,
|
}));
|
||||||
description: monitor.name,
|
const demoMonitors = await this.monitorsRepository.createMonitors(monitors as unknown as Monitor[]);
|
||||||
type: "http",
|
|
||||||
url: monitor.url,
|
|
||||||
interval: 60000,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const demoMonitors = await this.monitorsRepository.createMonitors(monitors);
|
|
||||||
|
|
||||||
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
|
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
|
||||||
return demoMonitors;
|
return demoMonitors;
|
||||||
@@ -226,7 +218,8 @@ export class MonitorService implements IMonitorService {
|
|||||||
checksData.monitorType !== "docker" &&
|
checksData.monitorType !== "docker" &&
|
||||||
checksData.monitorType !== "port" &&
|
checksData.monitorType !== "port" &&
|
||||||
checksData.monitorType !== "game" &&
|
checksData.monitorType !== "game" &&
|
||||||
checksData.monitorType !== "grpc"
|
checksData.monitorType !== "grpc" &&
|
||||||
|
checksData.monitorType !== "websocket"
|
||||||
) {
|
) {
|
||||||
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
||||||
}
|
}
|
||||||
@@ -340,7 +333,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (monitor.type !== "http" || !monitor.geoCheckEnabled) {
|
if (!supportsGeoCheck(monitor.type) || !monitor.geoCheckEnabled) {
|
||||||
return { groupedGeoChecks: [] };
|
return { groupedGeoChecks: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +429,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
return groups;
|
return groups;
|
||||||
};
|
};
|
||||||
|
|
||||||
editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => {
|
editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Partial<Monitor> }) => {
|
||||||
const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body);
|
const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body);
|
||||||
await this.jobQueue.updateJob(editedMonitor);
|
await this.jobQueue.updateJob(editedMonitor);
|
||||||
return editedMonitor;
|
return editedMonitor;
|
||||||
@@ -515,20 +508,6 @@ export class MonitorService implements IMonitorService {
|
|||||||
return deletedCount;
|
return deletedCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
sendTestEmail = async ({ to }: { to: string }): Promise<string> => {
|
|
||||||
const subject = "Test email from Checkmate";
|
|
||||||
const context = { testName: "Monitoring System" };
|
|
||||||
|
|
||||||
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
|
||||||
const messageId = await this.emailService.sendEmail(to, subject, html);
|
|
||||||
|
|
||||||
if (!messageId) {
|
|
||||||
throw new AppError({ message: "Failed to send test email.", service: SERVICE_NAME, method: "sendTestEmail", status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return messageId;
|
|
||||||
};
|
|
||||||
|
|
||||||
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<any[]> => {
|
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<any[]> => {
|
||||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
|
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,39 @@ import { canManageRole, type UserRole } from "@/types/user.js";
|
|||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||||
|
import { IEmailService } from "@/service/infrastructure/emailService.js";
|
||||||
|
import { EnvConfig, ISettingsService } from "@/service/system/settingsService.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import crypto from "crypto";
|
||||||
|
type CryptoType = typeof crypto;
|
||||||
|
type JwtType = typeof jwt;
|
||||||
const SERVICE_NAME = "userService";
|
const SERVICE_NAME = "userService";
|
||||||
|
|
||||||
class UserService {
|
export interface IUserService {
|
||||||
|
issueToken(payload: Partial<User>, appSettings: EnvConfig): string;
|
||||||
|
registerUser(user: Partial<User>, inviteToken: string, file: Express.Multer.File | null): Promise<{ user: User; token: string }>;
|
||||||
|
createUser(userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null): Promise<User>;
|
||||||
|
loginUser(email: string, password: string): Promise<{ user: User; token: string }>;
|
||||||
|
editUser(
|
||||||
|
updates: Partial<User & { newPassword?: string }>,
|
||||||
|
file: Express.Multer.File | null,
|
||||||
|
currentUserId: string,
|
||||||
|
currentUserEmail: string
|
||||||
|
): Promise<User>;
|
||||||
|
checkSuperadminExists(): Promise<boolean>;
|
||||||
|
requestRecovery(email: string): Promise<string | false | undefined>;
|
||||||
|
validateRecovery(recoveryToken: string): Promise<void>;
|
||||||
|
resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>;
|
||||||
|
deleteUser(params: { userId: string; teamId: string; roles: UserRole[] }): Promise<void>;
|
||||||
|
deleteUserById(params: { actorId: string; actorTeamId: string; actorRoles: UserRole[]; targetUserId: string }): Promise<void>;
|
||||||
|
getAllUsers(): Promise<User[]>;
|
||||||
|
getUserById(roles: UserRole[], userId: string): Promise<User>;
|
||||||
|
editUserById(userId: string, patch: Partial<User>): Promise<void>;
|
||||||
|
setPasswordByUserId(userId: string, password: string): Promise<User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserService implements IUserService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private hashPassword = (password: string): string => {
|
private hashPassword = (password: string): string => {
|
||||||
@@ -22,12 +51,12 @@ class UserService {
|
|||||||
return bcrypt.hashSync(password, salt);
|
return bcrypt.hashSync(password, salt);
|
||||||
};
|
};
|
||||||
|
|
||||||
private emailService: any;
|
private emailService: IEmailService;
|
||||||
private settingsService: any;
|
private settingsService: ISettingsService;
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
private jwt: any;
|
private jwt: JwtType;
|
||||||
private jobQueue: ISuperSimpleQueue;
|
private jobQueue: ISuperSimpleQueue;
|
||||||
private crypto: any;
|
private crypto: CryptoType;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
private usersRepository: IUsersRepository;
|
private usersRepository: IUsersRepository;
|
||||||
private invitesRepository: IInvitesRepository;
|
private invitesRepository: IInvitesRepository;
|
||||||
@@ -49,11 +78,11 @@ class UserService {
|
|||||||
settingsRepository,
|
settingsRepository,
|
||||||
teamsRepository,
|
teamsRepository,
|
||||||
}: {
|
}: {
|
||||||
crypto: any;
|
crypto: CryptoType;
|
||||||
emailService: any;
|
emailService: IEmailService;
|
||||||
settingsService: any;
|
settingsService: ISettingsService;
|
||||||
logger: any;
|
logger: ILogger;
|
||||||
jwt: any;
|
jwt: JwtType;
|
||||||
jobQueue: ISuperSimpleQueue;
|
jobQueue: ISuperSimpleQueue;
|
||||||
monitorsRepository: IMonitorsRepository;
|
monitorsRepository: IMonitorsRepository;
|
||||||
usersRepository: IUsersRepository;
|
usersRepository: IUsersRepository;
|
||||||
@@ -80,14 +109,14 @@ class UserService {
|
|||||||
return UserService.SERVICE_NAME;
|
return UserService.SERVICE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
issueToken = (payload: any, appSettings: any) => {
|
issueToken = (payload: Partial<User>, appSettings: EnvConfig) => {
|
||||||
const tokenTTL = appSettings?.jwtTTL ?? "2h";
|
const tokenTTL = appSettings?.jwtTTL ?? "2h";
|
||||||
const tokenSecret = appSettings?.jwtSecret;
|
const tokenSecret = appSettings?.jwtSecret;
|
||||||
const payloadData = payload;
|
const payloadData = payload;
|
||||||
return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
|
return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
|
||||||
};
|
};
|
||||||
|
|
||||||
registerUser = async (user: Partial<User>, inviteToken: string, file: any) => {
|
registerUser = async (user: Partial<User>, inviteToken: string, file: Express.Multer.File | null) => {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
// If superAdmin exists, a token should be attached to all further register requests
|
// If superAdmin exists, a token should be attached to all further register requests
|
||||||
const superAdminExists = await this.usersRepository.findSuperAdmin();
|
const superAdminExists = await this.usersRepository.findSuperAdmin();
|
||||||
@@ -120,7 +149,7 @@ class UserService {
|
|||||||
message: "New user created",
|
message: "New user created",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "registerUser",
|
method: "registerUser",
|
||||||
details: newUser.id,
|
details: { userId: newUser.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
delete newUser.profileImage;
|
delete newUser.profileImage;
|
||||||
@@ -134,27 +163,30 @@ class UserService {
|
|||||||
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
|
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
|
||||||
name: newUser.firstName,
|
name: newUser.firstName,
|
||||||
});
|
});
|
||||||
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: any) => {
|
if (!html) {
|
||||||
|
throw new Error("Failed to build welcome email HTML");
|
||||||
|
}
|
||||||
|
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: unknown) => {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "registerUser",
|
method: "registerUser",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "registerUser",
|
method: "registerUser",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user: newUser, token };
|
return { user: newUser, token };
|
||||||
};
|
};
|
||||||
|
|
||||||
createUser = async (userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: any) => {
|
createUser = async (userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null) => {
|
||||||
// Validate that the creator can assign the requested roles
|
// Validate that the creator can assign the requested roles
|
||||||
const targetRoles = userData.role ?? [];
|
const targetRoles = userData.role ?? [];
|
||||||
for (const targetRole of targetRoles) {
|
for (const targetRole of targetRoles) {
|
||||||
@@ -181,7 +213,7 @@ class UserService {
|
|||||||
message: "New user created by superadmin",
|
message: "New user created by superadmin",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "createUser",
|
method: "createUser",
|
||||||
details: newUser.id,
|
details: { userId: newUser.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
newUser.profileImage = undefined;
|
newUser.profileImage = undefined;
|
||||||
@@ -214,14 +246,19 @@ class UserService {
|
|||||||
return { user: userWithoutPassword, token };
|
return { user: userWithoutPassword, token };
|
||||||
};
|
};
|
||||||
|
|
||||||
editUser = async (updates: Partial<User & { newPassword?: string }>, file: any, currentUser: any) => {
|
editUser = async (
|
||||||
|
updates: Partial<User & { newPassword?: string }>,
|
||||||
|
file: Express.Multer.File | null,
|
||||||
|
currentUserId: string,
|
||||||
|
currentUserEmail: string
|
||||||
|
) => {
|
||||||
// Change Password check
|
// Change Password check
|
||||||
if (updates?.password && updates?.newPassword) {
|
if (updates?.password && updates?.newPassword) {
|
||||||
// Get user's email
|
// Get user's email
|
||||||
// Add user email to body for DB operation
|
// Add user email to body for DB operation
|
||||||
updates.email = currentUser.email;
|
updates.email = currentUserEmail;
|
||||||
// Get user
|
// Get user
|
||||||
const user = await this.usersRepository.findByEmail(currentUser.email);
|
const user = await this.usersRepository.findByEmail(currentUserEmail);
|
||||||
// Compare passwords
|
// Compare passwords
|
||||||
const match = await bcrypt.compare(updates?.password, user.password);
|
const match = await bcrypt.compare(updates?.password, user.password);
|
||||||
// If not a match, throw a 403
|
// If not a match, throw a 403
|
||||||
@@ -234,7 +271,7 @@ class UserService {
|
|||||||
delete updates.newPassword;
|
delete updates.newPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.usersRepository.updateById(currentUser.id, updates, file);
|
return await this.usersRepository.updateById(currentUserId, updates, file);
|
||||||
};
|
};
|
||||||
|
|
||||||
checkSuperadminExists = async () => {
|
checkSuperadminExists = async () => {
|
||||||
@@ -248,14 +285,22 @@ class UserService {
|
|||||||
await this.recoveryTokensRepository.deleteManyByEmail(email);
|
await this.recoveryTokensRepository.deleteManyByEmail(email);
|
||||||
const recoveryToken = await this.recoveryTokensRepository.create(email);
|
const recoveryToken = await this.recoveryTokensRepository.create(email);
|
||||||
const name = user.firstName;
|
const name = user.firstName;
|
||||||
const { clientHost } = this.settingsService.getSettings();
|
const settings = this.settingsService.getSettings();
|
||||||
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
|
const url = `${settings.clientHost!}/set-new-password/${recoveryToken.token}`;
|
||||||
|
|
||||||
const html = await this.emailService.buildEmail("passwordResetTemplate", {
|
const html = await this.emailService.buildEmail("passwordResetTemplate", {
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
|
if (!html) {
|
||||||
|
throw new AppError({
|
||||||
|
message: "Failed to build password reset email HTML",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "requestRecovery",
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html);
|
const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html);
|
||||||
return msgId;
|
return msgId;
|
||||||
};
|
};
|
||||||
@@ -286,24 +331,7 @@ class UserService {
|
|||||||
return { user: existingUser, token };
|
return { user: existingUser, token };
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteUser = async (user: User) => {
|
deleteUser = async ({ userId, teamId, roles }: { userId: string; teamId: string; roles: UserRole[] }) => {
|
||||||
const email = user?.email;
|
|
||||||
if (!email) {
|
|
||||||
throw new AppError({ message: "No email in request", service: SERVICE_NAME, method: "deleteUser", status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const teamId = user?.teamId;
|
|
||||||
const userId = user?.id;
|
|
||||||
|
|
||||||
if (!teamId) {
|
|
||||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new AppError({ message: "No user ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const roles = user?.role;
|
|
||||||
if (roles.includes("demo")) {
|
if (roles.includes("demo")) {
|
||||||
throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUser", status: 400 });
|
throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUser", status: 400 });
|
||||||
}
|
}
|
||||||
@@ -313,39 +341,87 @@ class UserService {
|
|||||||
|
|
||||||
if (roles.includes("superadmin")) {
|
if (roles.includes("superadmin")) {
|
||||||
// 2. Remove all jobs, delete checks and alerts
|
// 2. Remove all jobs, delete checks and alerts
|
||||||
res &&
|
if (res && res.length > 0) {
|
||||||
res?.length > 0 &&
|
await Promise.all(
|
||||||
(await Promise.all(
|
|
||||||
res.map(async (monitor) => {
|
res.map(async (monitor) => {
|
||||||
await this.jobQueue.deleteJob(monitor.id);
|
await this.jobQueue.deleteJob(monitor);
|
||||||
})
|
})
|
||||||
));
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 6. Delete the user by id
|
// 6. Delete the user by id
|
||||||
await this.usersRepository.deleteById(userId);
|
await this.usersRepository.deleteById(userId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
deleteUserById = async ({
|
||||||
|
actorId,
|
||||||
|
actorRoles,
|
||||||
|
actorTeamId,
|
||||||
|
targetUserId,
|
||||||
|
}: {
|
||||||
|
actorId: string;
|
||||||
|
actorTeamId: string;
|
||||||
|
actorRoles: UserRole[];
|
||||||
|
targetUserId: string;
|
||||||
|
}) => {
|
||||||
|
if (actorId === targetUserId) {
|
||||||
|
throw new AppError({ message: "Cannot delete your own account from here", service: SERVICE_NAME, method: "deleteUserById", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUser = await this.usersRepository.findById(targetUserId);
|
||||||
|
|
||||||
|
if (targetUser.teamId !== actorTeamId) {
|
||||||
|
throw new AppError({ message: "User is not on your team", service: SERVICE_NAME, method: "deleteUserById", status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUser.role.includes("demo")) {
|
||||||
|
throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUserById", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetRoles = targetUser.role;
|
||||||
|
|
||||||
|
// Check actor can manage all of target's roles
|
||||||
|
for (const targetRole of targetRoles) {
|
||||||
|
const canManage = actorRoles.some((actorRole) => canManageRole(actorRole, targetRole));
|
||||||
|
if (!canManage) {
|
||||||
|
throw new AppError({
|
||||||
|
message: "You do not have permission to remove this user",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "deleteUserById",
|
||||||
|
status: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.usersRepository.deleteById(targetUserId);
|
||||||
|
|
||||||
|
this.logger.info({
|
||||||
|
message: `User ${targetUserId} deleted by ${actorId}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "deleteUserById",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
getAllUsers = async () => {
|
getAllUsers = async () => {
|
||||||
return await this.usersRepository.findAll();
|
return await this.usersRepository.findAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
getUserById = async (roles: any, userId: any) => {
|
getUserById = async (roles: UserRole[], userId: string) => {
|
||||||
if (!roles.includes("superadmin")) {
|
if (!roles.includes("superadmin") && !roles.includes("admin")) {
|
||||||
throw new AppError({ message: "User is not a superadmin", service: SERVICE_NAME, status: 403 });
|
throw new AppError({ message: "Insufficient permissions", service: SERVICE_NAME, status: 403 });
|
||||||
}
|
}
|
||||||
const user = await this.usersRepository.findById(userId);
|
const user = await this.usersRepository.findById(userId);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
editUserById = async (userId: any, patch: Partial<User>) => {
|
editUserById = async (userId: string, patch: Partial<User>) => {
|
||||||
await this.usersRepository.updateById(userId, patch, null);
|
await this.usersRepository.updateById(userId, patch, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
setPasswordByUserId = async (userId: any, password: string) => {
|
setPasswordByUserId = async (userId: string, password: string) => {
|
||||||
const hashedPassword = this.hashPassword(password);
|
const hashedPassword = this.hashPassword(password);
|
||||||
const updatedUser = await this.usersRepository.updateById(userId, { password: hashedPassword }, null);
|
const updatedUser = await this.usersRepository.updateById(userId, { password: hashedPassword }, null);
|
||||||
return updatedUser;
|
return updatedUser;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export default UserService;
|
|
||||||
@@ -1,14 +1,34 @@
|
|||||||
|
// Business services
|
||||||
|
export * from "@/service/business/checkService.js";
|
||||||
|
export * from "@/service/business/diagnosticService.js";
|
||||||
|
export * from "@/service/business/geoChecksService.js";
|
||||||
|
export * from "@/service/business/incidentService.js";
|
||||||
|
export * from "@/service/business/inviteService.js";
|
||||||
|
export * from "@/service/business/maintenanceWindowService.js";
|
||||||
export * from "@/service/business/monitorService.js";
|
export * from "@/service/business/monitorService.js";
|
||||||
|
export * from "@/service/business/statusPageService.js";
|
||||||
|
export * from "@/service/business/userService.js";
|
||||||
|
|
||||||
|
// Infrastructure services
|
||||||
|
export * from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||||
|
export * from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||||
|
export * from "@/service/infrastructure/notificationMessageBuilder.js";
|
||||||
|
export * from "@/service/infrastructure/bufferService.js";
|
||||||
|
export * from "@/service/infrastructure/emailService.js";
|
||||||
|
export * from "@/service/infrastructure/globalPingService.js";
|
||||||
export * from "@/service/infrastructure/networkService.js";
|
export * from "@/service/infrastructure/networkService.js";
|
||||||
|
export * from "@/service/infrastructure/notificationsService.js";
|
||||||
|
export * from "@/service/infrastructure/statusService.js";
|
||||||
|
|
||||||
|
// Notification providers
|
||||||
export * from "@/service/infrastructure/notificationProviders/discord.js";
|
export * from "@/service/infrastructure/notificationProviders/discord.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/email.js";
|
export * from "@/service/infrastructure/notificationProviders/email.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js";
|
export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js";
|
||||||
|
export * from "@/service/infrastructure/notificationProviders/matrix.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/pagerduty.js";
|
export * from "@/service/infrastructure/notificationProviders/pagerduty.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/matrix.js";
|
|
||||||
export * from "@/service/infrastructure/notificationProviders/slack.js";
|
export * from "@/service/infrastructure/notificationProviders/slack.js";
|
||||||
|
export * from "@/service/infrastructure/notificationProviders/teams.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
||||||
export * from "@/service/infrastructure/notificationsService.js";
|
|
||||||
export * from "@/service/infrastructure/statusService.js";
|
// System services
|
||||||
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
export * from "@/service/system/settingsService.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/matrix.js";
|
|
||||||
export * from "@/service/business/statusPageService.js";
|
|
||||||
@@ -2,6 +2,7 @@ import { IMonitorsRepository } from "@/repositories/index.js";
|
|||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
import Scheduler from "super-simple-scheduler";
|
import Scheduler from "super-simple-scheduler";
|
||||||
import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||||
|
import { Monitor, MonitorType, supportsGeoCheck } from "@/types/monitor.js";
|
||||||
const SERVICE_NAME = "JobQueue";
|
const SERVICE_NAME = "JobQueue";
|
||||||
|
|
||||||
type QueueJobFailure = {
|
type QueueJobFailure = {
|
||||||
@@ -41,11 +42,11 @@ type QueueJobSummary = {
|
|||||||
export interface ISuperSimpleQueue {
|
export interface ISuperSimpleQueue {
|
||||||
readonly serviceName: string;
|
readonly serviceName: string;
|
||||||
init(): Promise<boolean>;
|
init(): Promise<boolean>;
|
||||||
addJob(monitorId: string, monitor: any): Promise<void>;
|
addJob(monitorId: string, monitor: Monitor): Promise<void>;
|
||||||
deleteJob(monitor: any): Promise<void>;
|
deleteJob(monitor: Monitor): Promise<void>;
|
||||||
pauseJob(monitor: any): Promise<void>;
|
pauseJob(monitor: Monitor): Promise<void>;
|
||||||
resumeJob(monitor: any): Promise<void>;
|
resumeJob(monitor: Monitor): Promise<void>;
|
||||||
updateJob(monitor: any): Promise<void>;
|
updateJob(monitor: Monitor): Promise<void>;
|
||||||
shutdown(): Promise<void>;
|
shutdown(): Promise<void>;
|
||||||
getMetrics(): Promise<QueueMetrics>;
|
getMetrics(): Promise<QueueMetrics>;
|
||||||
getJobs(): Promise<QueueJobSummary[]>;
|
getJobs(): Promise<QueueJobSummary[]>;
|
||||||
@@ -53,7 +54,7 @@ export interface ISuperSimpleQueue {
|
|||||||
obliterate(): Promise<void>;
|
obliterate(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SuperSimpleQueue implements ISuperSimpleQueue {
|
export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -88,9 +89,10 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
try {
|
try {
|
||||||
this.scheduler.start();
|
this.scheduler.start();
|
||||||
|
|
||||||
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
this.scheduler.addTemplate("monitor-job", this.helper.getHeartbeatJob());
|
||||||
this.scheduler.addTemplate("geo-check-job", this.helper.getGeoCheckJob());
|
this.scheduler.addTemplate("geo-check-job", this.helper.getHeartbeatGeoJob());
|
||||||
this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob());
|
this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob());
|
||||||
|
this.scheduler.addTemplate("cleanup-retention-job", this.helper.getCleanupRetentionJob());
|
||||||
const monitors = await this.monitorsRepository.findAll();
|
const monitors = await this.monitorsRepository.findAll();
|
||||||
if (!monitors) {
|
if (!monitors) {
|
||||||
return true;
|
return true;
|
||||||
@@ -103,20 +105,20 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.scheduler.addJob({ id: "cleanup-orphaned", template: "cleanup-orphaned", active: true });
|
this.scheduler.addJob({ id: "cleanup-orphaned", template: "cleanup-orphaned", active: true });
|
||||||
|
this.scheduler.addJob({ id: "cleanup-retention", template: "cleanup-retention-job", active: true, repeat: 24 * 60 * 60 * 1000 });
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: "Failed to initialize SuperSimpleQueue",
|
message: `Failed to initialize SuperSimpleQueue: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "init",
|
method: "init",
|
||||||
details: error,
|
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
addJob = async (monitorId: string, monitor: any) => {
|
addJob = async (monitorId: string, monitor: Monitor) => {
|
||||||
this.scheduler.addJob({
|
this.scheduler.addJob({
|
||||||
id: monitorId,
|
id: monitorId,
|
||||||
template: "monitor-job",
|
template: "monitor-job",
|
||||||
@@ -125,8 +127,13 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
data: monitor,
|
data: monitor,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Return early if we don't need geo checks
|
||||||
|
if (!supportsGeoCheck(monitor.type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add geo check job if enabled for HTTP monitors
|
// Add geo check job if enabled for HTTP monitors
|
||||||
if (monitor.geoCheckEnabled && monitor.type === "http") {
|
if (monitor.geoCheckEnabled) {
|
||||||
this.scheduler.addJob({
|
this.scheduler.addJob({
|
||||||
id: `${monitorId}-geo`,
|
id: `${monitorId}-geo`,
|
||||||
template: "geo-check-job",
|
template: "geo-check-job",
|
||||||
@@ -137,12 +144,12 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteJob = async (monitor: any) => {
|
deleteJob = async (monitor: Monitor) => {
|
||||||
this.scheduler.removeJob(monitor.id);
|
this.scheduler.removeJob(monitor.id);
|
||||||
this.scheduler.removeJob(`${monitor.id}-geo`);
|
this.scheduler.removeJob(`${monitor.id}-geo`);
|
||||||
};
|
};
|
||||||
|
|
||||||
pauseJob = async (monitor: any) => {
|
pauseJob = async (monitor: Monitor) => {
|
||||||
const result = await this.scheduler.pauseJob(monitor.id);
|
const result = await this.scheduler.pauseJob(monitor.id);
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
throw new Error("Failed to pause monitor");
|
throw new Error("Failed to pause monitor");
|
||||||
@@ -155,7 +162,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
resumeJob = async (monitor: any) => {
|
resumeJob = async (monitor: Monitor) => {
|
||||||
const result = await this.scheduler.resumeJob(monitor.id);
|
const result = await this.scheduler.resumeJob(monitor.id);
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
throw new Error("Failed to resume monitor");
|
throw new Error("Failed to resume monitor");
|
||||||
@@ -170,12 +177,12 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
updateJob = async (monitor: any) => {
|
updateJob = async (monitor: Monitor) => {
|
||||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
||||||
|
|
||||||
// Handle geo check job lifecycle
|
// Handle geo check job lifecycle
|
||||||
const geoJobId = `${monitor.id}-geo`;
|
const geoJobId = `${monitor.id}-geo`;
|
||||||
if (monitor.geoCheckEnabled && monitor.type === "http") {
|
if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) {
|
||||||
// Check if geo job exists
|
// Check if geo job exists
|
||||||
const existingGeoJob = await this.scheduler.getJob(geoJobId);
|
const existingGeoJob = await this.scheduler.getJob(geoJobId);
|
||||||
if (existingGeoJob) {
|
if (existingGeoJob) {
|
||||||
@@ -238,8 +245,8 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
failingJobs: 0,
|
failingJobs: 0,
|
||||||
jobsWithFailures: [] as Array<{
|
jobsWithFailures: [] as Array<{
|
||||||
monitorId: string | number;
|
monitorId: string | number;
|
||||||
monitorUrl: any;
|
monitorUrl: string;
|
||||||
monitorType: any;
|
monitorType: MonitorType;
|
||||||
failedAt: number | null;
|
failedAt: number | null;
|
||||||
failCount: number;
|
failCount: number;
|
||||||
failReason: string | null;
|
failReason: string | null;
|
||||||
@@ -289,5 +296,3 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SuperSimpleQueue;
|
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
const SERVICE_NAME = "JobQueueHelper";
|
const SERVICE_NAME = "JobQueueHelper";
|
||||||
import type { Monitor } from "@/types/monitor.js";
|
import type { Monitor } from "@/types/monitor.js";
|
||||||
|
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
|
import {
|
||||||
import type { StatusChangeResult } from "@/types/index.js";
|
ICheckService,
|
||||||
import IncidentService from "@/service/business/incidentService.js";
|
INetworkService,
|
||||||
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
INotificationsService,
|
||||||
|
ISettingsService,
|
||||||
|
IStatusService,
|
||||||
|
IncidentService,
|
||||||
|
type IGeoChecksService,
|
||||||
|
} from "@/service/index.js";
|
||||||
|
import { CHECK_TTL_SENTINEL, type MaintenanceWindow, type StatusChangeResult } from "@/types/index.js";
|
||||||
import {
|
import {
|
||||||
IMaintenanceWindowsRepository,
|
IMaintenanceWindowsRepository,
|
||||||
IMonitorsRepository,
|
IMonitorsRepository,
|
||||||
@@ -15,13 +22,14 @@ import {
|
|||||||
IGeoChecksRepository,
|
IGeoChecksRepository,
|
||||||
} from "@/repositories/index.js";
|
} from "@/repositories/index.js";
|
||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
import { IBufferService } from "../bufferService.js";
|
import { IBufferService } from "@/service/index.js";
|
||||||
|
|
||||||
export interface ISuperSimpleQueueHelper {
|
export interface ISuperSimpleQueueHelper {
|
||||||
readonly serviceName: string;
|
readonly serviceName: string;
|
||||||
getMonitorJob(): (monitor: Monitor) => Promise<void>;
|
getHeartbeatJob(): (monitor: Monitor) => Promise<void>;
|
||||||
|
getHeartbeatGeoJob(): (monitor: Monitor) => Promise<void>;
|
||||||
getCleanupOrphanedJob(): () => Promise<void>;
|
getCleanupOrphanedJob(): () => Promise<void>;
|
||||||
getGeoCheckJob(): (monitor: Monitor) => Promise<void>;
|
getCleanupRetentionJob(): () => Promise<void>;
|
||||||
isInMaintenanceWindow(monitorId: string, teamId: string): Promise<boolean>;
|
isInMaintenanceWindow(monitorId: string, teamId: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,14 +47,15 @@ export interface MonitorActionDecision {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
private networkService: INetworkService;
|
private networkService: INetworkService;
|
||||||
private statusService: IStatusService;
|
private statusService: IStatusService;
|
||||||
private notificationsService: INotificationsService;
|
private notificationsService: INotificationsService;
|
||||||
private checkService: any;
|
private checkService: ICheckService;
|
||||||
|
private settingsService: ISettingsService;
|
||||||
private buffer: IBufferService;
|
private buffer: IBufferService;
|
||||||
private incidentService: IncidentService;
|
private incidentService: IncidentService;
|
||||||
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||||
@@ -63,7 +72,8 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
networkService: INetworkService,
|
networkService: INetworkService,
|
||||||
statusService: IStatusService,
|
statusService: IStatusService,
|
||||||
notificationsService: INotificationsService,
|
notificationsService: INotificationsService,
|
||||||
checkService: any,
|
checkService: ICheckService,
|
||||||
|
settingsService: ISettingsService,
|
||||||
buffer: IBufferService,
|
buffer: IBufferService,
|
||||||
incidentService: IncidentService,
|
incidentService: IncidentService,
|
||||||
maintenanceWindowsRepository: IMaintenanceWindowsRepository,
|
maintenanceWindowsRepository: IMaintenanceWindowsRepository,
|
||||||
@@ -79,6 +89,7 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
this.networkService = networkService;
|
this.networkService = networkService;
|
||||||
this.statusService = statusService;
|
this.statusService = statusService;
|
||||||
this.checkService = checkService;
|
this.checkService = checkService;
|
||||||
|
this.settingsService = settingsService;
|
||||||
this.buffer = buffer;
|
this.buffer = buffer;
|
||||||
this.notificationsService = notificationsService;
|
this.notificationsService = notificationsService;
|
||||||
this.incidentService = incidentService;
|
this.incidentService = incidentService;
|
||||||
@@ -96,7 +107,7 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
return SuperSimpleQueueHelper.SERVICE_NAME;
|
return SuperSimpleQueueHelper.SERVICE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMonitorJob = () => {
|
getHeartbeatJob = () => {
|
||||||
return async (monitor: Monitor) => {
|
return async (monitor: Monitor) => {
|
||||||
try {
|
try {
|
||||||
const monitorId = monitor.id;
|
const monitorId = monitor.id;
|
||||||
@@ -127,7 +138,7 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 3. Build check
|
// Step 3. Build check
|
||||||
const check = await this.checkService.buildCheck(status);
|
const check = this.checkService.buildCheck(status);
|
||||||
if (!check) {
|
if (!check) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `No check could be built for monitor ${monitorId}`,
|
message: `No check could be built for monitor ${monitorId}`,
|
||||||
@@ -147,33 +158,31 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
|
|
||||||
// Step 6. Handle notifications (best effort, continue even in event of failure, don't wait)
|
// Step 6. Handle notifications (best effort, continue even in event of failure, don't wait)
|
||||||
if (decision.shouldSendNotification) {
|
if (decision.shouldSendNotification) {
|
||||||
this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: any) => {
|
this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: unknown) => {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getMonitorJob",
|
method: "getMonitorJob",
|
||||||
details: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error.message}`,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
stack: error.stack,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 7. Handle incidents (best effort, don't wait)
|
// Step 7. Handle incidents (best effort, don't wait)
|
||||||
this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: any) => {
|
this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: unknown) => {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: error.message,
|
message: `Error handling incident for job ${monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getMonitorJob",
|
method: "getMonitorJob",
|
||||||
details: `Error handling incident for job ${monitor.id}: ${error.message}`,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
stack: error.stack,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: error.service || SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: error.method || "getMonitorJob",
|
method: "getMonitorJob",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -259,19 +268,19 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getCleanupOrphanedJob",
|
method: "getCleanupOrphanedJob",
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getCleanupOrphanedJob",
|
method: "getCleanupOrphanedJob",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
getGeoCheckJob = () => {
|
getHeartbeatGeoJob = () => {
|
||||||
return async (monitor: Monitor) => {
|
return async (monitor: Monitor) => {
|
||||||
try {
|
try {
|
||||||
const monitorId = monitor.id;
|
const monitorId = monitor.id;
|
||||||
@@ -279,27 +288,26 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
|
|
||||||
// Step 1: Validate monitor eligibility
|
// Step 1: Validate monitor eligibility
|
||||||
if (!monitorId) {
|
if (!monitorId) {
|
||||||
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
|
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getHeartbeatGeoJob" });
|
||||||
}
|
|
||||||
|
|
||||||
if (monitor.type !== "http") {
|
|
||||||
this.logger.debug({
|
|
||||||
message: `Monitor ${monitorId} is not HTTP type, skipping geo check`,
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
method: "getGeoCheckJob",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!monitor.geoCheckEnabled) {
|
if (!monitor.geoCheckEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!supportsGeoCheck(monitor.type)) {
|
||||||
|
this.logger.debug({
|
||||||
|
message: `Monitor ${monitorId} type does not support geo checks, skipping`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getHeartbeatGeoJob",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
|
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `No geo check locations configured for monitor ${monitorId}`,
|
message: `No geo check locations configured for monitor ${monitorId}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getGeoCheckJob",
|
method: "getHeartbeatGeoJob",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -310,7 +318,7 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
this.logger.debug({
|
this.logger.debug({
|
||||||
message: `Monitor ${monitorId} is in maintenance window, skipping geo check`,
|
message: `Monitor ${monitorId} is in maintenance window, skipping geo check`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getGeoCheckJob",
|
method: "getHeartbeatGeoJob",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -321,7 +329,7 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `No geo check could be built for monitor ${monitorId}`,
|
message: `No geo check could be built for monitor ${monitorId}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getGeoCheckJob",
|
method: "getHeartbeatGeoJob",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -332,14 +340,14 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
this.logger.debug({
|
this.logger.debug({
|
||||||
message: `Geo check job executed for monitor ${monitorId}`,
|
message: `Geo check job executed for monitor ${monitorId}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getGeoCheckJob",
|
method: "getHeartbeatGeoJob",
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getGeoCheckJob",
|
method: "getHeartbeatGeoJob",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
// Don't throw - geo check failures shouldn't crash the job scheduler
|
// Don't throw - geo check failures shouldn't crash the job scheduler
|
||||||
}
|
}
|
||||||
@@ -349,7 +357,7 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
async isInMaintenanceWindow(monitorId: string, teamId: string) {
|
async isInMaintenanceWindow(monitorId: string, teamId: string) {
|
||||||
const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId);
|
const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId);
|
||||||
// Check for active maintenance window:
|
// Check for active maintenance window:
|
||||||
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => {
|
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: boolean, window: MaintenanceWindow) => {
|
||||||
if (window.active) {
|
if (window.active) {
|
||||||
const start = new Date(window.start);
|
const start = new Date(window.start);
|
||||||
const end = new Date(window.end);
|
const end = new Date(window.end);
|
||||||
@@ -376,6 +384,40 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
return maintenanceWindowIsActive;
|
return maintenanceWindowIsActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCleanupRetentionJob = () => {
|
||||||
|
return async () => {
|
||||||
|
try {
|
||||||
|
const settings = await this.settingsService.getDBSettings();
|
||||||
|
|
||||||
|
const checkTTL = settings.checkTTL; // Check TTL is in DAYS, not MS
|
||||||
|
|
||||||
|
if (checkTTL === CHECK_TTL_SENTINEL) {
|
||||||
|
this.logger.info({
|
||||||
|
message: `Check TTL is set to unlimited, skipping cleanup`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getCleanupRetentionJob",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const checkTTLInMs = checkTTL * 24 * 60 * 60 * 1000;
|
||||||
|
const cutoffDate = new Date(Date.now() - checkTTLInMs);
|
||||||
|
const deleteCount = await this.checkService.deleteOlderThan(cutoffDate);
|
||||||
|
this.logger.info({
|
||||||
|
message: `Deleted ${deleteCount} checks older than ${cutoffDate.toISOString()}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getCleanupRetentionJob",
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error({
|
||||||
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getCleanupRetentionJob",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
private evaluateMonitorAction(statusChangeResult: StatusChangeResult): MonitorActionDecision {
|
private evaluateMonitorAction(statusChangeResult: StatusChangeResult): MonitorActionDecision {
|
||||||
const { monitor, statusChanged, prevStatus } = statusChangeResult;
|
const { monitor, statusChanged, prevStatus } = statusChangeResult;
|
||||||
|
|
||||||
@@ -414,5 +456,3 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
return decision;
|
return decision;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SuperSimpleQueueHelper;
|
|
||||||
@@ -15,7 +15,7 @@ export interface IBufferService {
|
|||||||
flushGeoBuffer(): Promise<void>;
|
flushGeoBuffer(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class BufferService implements IBufferService {
|
export class BufferService implements IBufferService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private BUFFER_TIMEOUT: number;
|
private BUFFER_TIMEOUT: number;
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -177,5 +177,3 @@ class BufferService implements IBufferService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BufferService;
|
|
||||||
@@ -23,7 +23,7 @@ export interface IEmailService {
|
|||||||
sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise<string | false | undefined>;
|
sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise<string | false | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmailService implements IEmailService {
|
export class EmailService implements IEmailService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private settingsService: ISettingsService;
|
private settingsService: ISettingsService;
|
||||||
@@ -94,7 +94,7 @@ class EmailService implements IEmailService {
|
|||||||
if (!mjml) {
|
if (!mjml) {
|
||||||
throw new Error(`Template ${template} not found`);
|
throw new Error(`Template ${template} not found`);
|
||||||
}
|
}
|
||||||
const html = this.mjml2html(mjml);
|
const html = await this.mjml2html(mjml);
|
||||||
return html.html;
|
return html.html;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
@@ -178,5 +178,3 @@ class EmailService implements IEmailService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EmailService;
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js";
|
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js";
|
||||||
|
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||||
|
import { MonitorType } from "@/types/index.js";
|
||||||
import type { ILogger } from "@/utils/logger.js";
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
|
|
||||||
@@ -8,7 +10,7 @@ const POLL_INTERVAL_MS = 2000;
|
|||||||
const MAX_POLL_TIMEOUT_MS = 30000;
|
const MAX_POLL_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
interface GlobalPingMeasurementRequest {
|
interface GlobalPingMeasurementRequest {
|
||||||
type: "http";
|
type: MonitorType;
|
||||||
target: string;
|
target: string;
|
||||||
locations: Array<{ continent: GeoContinent }>;
|
locations: Array<{ continent: GeoContinent }>;
|
||||||
limit: number;
|
limit: number;
|
||||||
@@ -44,17 +46,26 @@ interface GlobalPingProbeResult {
|
|||||||
firstByte: number;
|
firstByte: number;
|
||||||
download: number;
|
download: number;
|
||||||
};
|
};
|
||||||
|
stats?: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
avg: number;
|
||||||
|
total: number;
|
||||||
|
loss: number;
|
||||||
|
rcv: number;
|
||||||
|
drop: number;
|
||||||
|
};
|
||||||
rawOutput?: string;
|
rawOutput?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGlobalPingService {
|
export interface IGlobalPingService {
|
||||||
readonly serviceName: string;
|
readonly serviceName: string;
|
||||||
createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null>;
|
createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null>;
|
||||||
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
|
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GlobalPingService implements IGlobalPingService {
|
export class GlobalPingService implements IGlobalPingService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -67,13 +78,16 @@ class GlobalPingService implements IGlobalPingService {
|
|||||||
return GlobalPingService.SERVICE_NAME;
|
return GlobalPingService.SERVICE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null> {
|
async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
|
if (!supportsGeoCheck(monitorType)) {
|
||||||
|
throw new Error(`Unsupported monitor type for GlobalPing: ${monitorType}`);
|
||||||
|
}
|
||||||
// GlobalPing API expects target without protocol (http:// or https://)
|
// GlobalPing API expects target without protocol (http:// or https://)
|
||||||
const cleanTarget = url.replace(/^https?:\/\//, "");
|
const cleanTarget = url.replace(/^https?:\/\//, "");
|
||||||
|
|
||||||
const requestBody: GlobalPingMeasurementRequest = {
|
const requestBody: GlobalPingMeasurementRequest = {
|
||||||
type: "http",
|
type: monitorType,
|
||||||
target: cleanTarget,
|
target: cleanTarget,
|
||||||
locations: locations.map((continent) => ({ continent })),
|
locations: locations.map((continent) => ({ continent })),
|
||||||
limit: locations.length,
|
limit: locations.length,
|
||||||
@@ -88,10 +102,9 @@ class GlobalPingService implements IGlobalPingService {
|
|||||||
const measurementId = response.body.id;
|
const measurementId = response.body.id;
|
||||||
|
|
||||||
this.logger.debug({
|
this.logger.debug({
|
||||||
message: `Created GlobalPing measurement: ${measurementId}`,
|
message: `Created GlobalPing measurement: ${measurementId} for target: ${cleanTarget}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "createMeasurement",
|
method: "createMeasurement",
|
||||||
details: { measurementId, url, locations },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return measurementId;
|
return measurementId;
|
||||||
@@ -165,8 +178,7 @@ class GlobalPingService implements IGlobalPingService {
|
|||||||
const successfulResults: GeoCheckResult[] = [];
|
const successfulResults: GeoCheckResult[] = [];
|
||||||
|
|
||||||
for (const probeResult of probeResults) {
|
for (const probeResult of probeResults) {
|
||||||
// Skip failed or timeout results
|
if (probeResult.result.status !== "finished") {
|
||||||
if (probeResult.result.status !== "finished" || !probeResult.result.statusCode || !probeResult.result.timings) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,23 +192,38 @@ class GlobalPingService implements IGlobalPingService {
|
|||||||
latitude: probeResult.probe.latitude,
|
latitude: probeResult.probe.latitude,
|
||||||
};
|
};
|
||||||
|
|
||||||
const timings: GeoCheckTimings = {
|
// HTTP results have statusCode and timings, ping results have stats
|
||||||
total: probeResult.result.timings.total,
|
if (probeResult.result.statusCode && probeResult.result.timings) {
|
||||||
dns: probeResult.result.timings.dns,
|
const timings: GeoCheckTimings = {
|
||||||
tcp: probeResult.result.timings.tcp,
|
total: probeResult.result.timings.total,
|
||||||
tls: probeResult.result.timings.tls,
|
dns: probeResult.result.timings.dns,
|
||||||
firstByte: probeResult.result.timings.firstByte,
|
tcp: probeResult.result.timings.tcp,
|
||||||
download: probeResult.result.timings.download,
|
tls: probeResult.result.timings.tls,
|
||||||
};
|
firstByte: probeResult.result.timings.firstByte,
|
||||||
|
download: probeResult.result.timings.download,
|
||||||
|
};
|
||||||
|
|
||||||
const result: GeoCheckResult = {
|
successfulResults.push({
|
||||||
location,
|
location,
|
||||||
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
|
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
|
||||||
statusCode: probeResult.result.statusCode,
|
statusCode: probeResult.result.statusCode,
|
||||||
timings,
|
timings,
|
||||||
};
|
});
|
||||||
|
} else if (probeResult.result.stats) {
|
||||||
successfulResults.push(result);
|
successfulResults.push({
|
||||||
|
location,
|
||||||
|
status: probeResult.result.stats.loss === 0,
|
||||||
|
statusCode: probeResult.result.stats.loss === 0 ? 200 : 5000,
|
||||||
|
timings: {
|
||||||
|
total: probeResult.result.stats.avg,
|
||||||
|
dns: 0,
|
||||||
|
tcp: 0,
|
||||||
|
tls: 0,
|
||||||
|
firstByte: 0,
|
||||||
|
download: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return successfulResults;
|
return successfulResults;
|
||||||
@@ -206,5 +233,3 @@ class GlobalPingService implements IGlobalPingService {
|
|||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GlobalPingService;
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Monitor } from "@/types/monitor.js";
|
||||||
|
import jmespath from "jmespath";
|
||||||
|
type JmesPath = typeof jmespath;
|
||||||
|
|
||||||
|
export class AdvancedMatcher {
|
||||||
|
constructor(private jmespath: JmesPath) {}
|
||||||
|
|
||||||
|
private compare(actual: unknown, expected: string, method?: string): boolean {
|
||||||
|
if (method === "equal") return String(actual) === expected;
|
||||||
|
if (method === "include") return String(actual).includes(expected);
|
||||||
|
if (method === "regex") return new RegExp(expected).test(String(actual));
|
||||||
|
return String(actual) === expected; // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
validate<T>(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: any } {
|
||||||
|
const { useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor;
|
||||||
|
if (!useAdvancedMatching) return { ok: true, message: "Success" };
|
||||||
|
|
||||||
|
let dataToValidate = payload;
|
||||||
|
|
||||||
|
if (jsonPath) {
|
||||||
|
try {
|
||||||
|
dataToValidate = this.jmespath.search(payload, jsonPath);
|
||||||
|
} catch {
|
||||||
|
return { ok: false, message: "Error evaluating JSON path" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedValue) {
|
||||||
|
const ok = this.compare(dataToValidate, expectedValue, matchMethod);
|
||||||
|
return {
|
||||||
|
ok,
|
||||||
|
message: ok ? "Success" : "Expected value did not match",
|
||||||
|
extracted: dataToValidate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFalsey = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null;
|
||||||
|
return {
|
||||||
|
ok: !isFalsey,
|
||||||
|
message: !isFalsey ? "Success" : "Extracted value is falsey",
|
||||||
|
extracted: dataToValidate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||||
|
import { DockerStatusPayload, MonitorStatusResponse } from "@/types/network.js";
|
||||||
|
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import Dockerode from "dockerode";
|
||||||
|
import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js";
|
||||||
|
|
||||||
|
type DockerodeType = typeof Dockerode;
|
||||||
|
|
||||||
|
const SERVICE_NAME = "DockerProvider";
|
||||||
|
|
||||||
|
export interface DockerError extends Error {
|
||||||
|
statusCode?: number;
|
||||||
|
reason?: string;
|
||||||
|
json?: { message?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DockerProvider implements IStatusProvider<DockerStatusPayload> {
|
||||||
|
readonly type = "docker";
|
||||||
|
private docker: Dockerode;
|
||||||
|
constructor(
|
||||||
|
private logger: ILogger,
|
||||||
|
private DockerLib: DockerodeType
|
||||||
|
) {
|
||||||
|
this.docker = new this.DockerLib();
|
||||||
|
}
|
||||||
|
|
||||||
|
supports(type: MonitorType): boolean {
|
||||||
|
return type === "docker";
|
||||||
|
}
|
||||||
|
|
||||||
|
private isDockerError(error: unknown): error is DockerError {
|
||||||
|
return error instanceof Error && ("statusCode" in error || "reason" in error || "json" in error);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(monitor: Monitor): Promise<MonitorStatusResponse<DockerStatusPayload>> {
|
||||||
|
const { url: containerInput } = monitor;
|
||||||
|
try {
|
||||||
|
if (!containerInput) {
|
||||||
|
throw new Error("Container name or ID is required for Docker monitor");
|
||||||
|
}
|
||||||
|
|
||||||
|
const containers = await this.docker.listContainers({ all: true });
|
||||||
|
const normalizedInput = containerInput.replace(/^\/+/, "").toLowerCase();
|
||||||
|
|
||||||
|
// Priority-based matching to avoid ambiguity:
|
||||||
|
// 1. Exact full ID match (64-char)
|
||||||
|
const exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput);
|
||||||
|
|
||||||
|
// 2. Exact container name match (case-insensitive)
|
||||||
|
const exactNameMatch = containers.find((c) =>
|
||||||
|
c.Names.some((name: string) => {
|
||||||
|
const cleanName = name.replace(/^\/+/, "").toLowerCase();
|
||||||
|
return cleanName === normalizedInput;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Partial ID match (fallback for backwards compatibility)
|
||||||
|
const partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput));
|
||||||
|
|
||||||
|
// Select container based on priority
|
||||||
|
const targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
|
||||||
|
|
||||||
|
// Handle no match
|
||||||
|
if (!targetContainer) {
|
||||||
|
this.logger.warn({
|
||||||
|
message: `No container found for "${monitor.url}".`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "handle",
|
||||||
|
details: { url: monitor.url },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: false,
|
||||||
|
code: 404,
|
||||||
|
message: "Docker container not found",
|
||||||
|
responseTime: 0,
|
||||||
|
payload: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Handle Ambiguity Check
|
||||||
|
const matchTypes: string[] = [];
|
||||||
|
if (exactIdMatch) matchTypes.push("exact ID");
|
||||||
|
if (exactNameMatch) matchTypes.push("exact name");
|
||||||
|
if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID");
|
||||||
|
|
||||||
|
if (matchTypes.length > 1) {
|
||||||
|
const message = `Ambiguous container match for "${containerInput}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`;
|
||||||
|
|
||||||
|
this.logger.warn({
|
||||||
|
message,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "handle",
|
||||||
|
details: { url: containerInput },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: false,
|
||||||
|
code: NETWORK_ERROR,
|
||||||
|
message,
|
||||||
|
responseTime: 0,
|
||||||
|
payload: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Inspect Container Status
|
||||||
|
const container = this.docker.getContainer(targetContainer.Id);
|
||||||
|
const { response, responseTime, error } = await timeRequest(() => container.inspect());
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
let message = "Failed to fetch Docker container information";
|
||||||
|
let code = NETWORK_ERROR;
|
||||||
|
|
||||||
|
if (this.isDockerError(error)) {
|
||||||
|
code = error.statusCode ?? NETWORK_ERROR;
|
||||||
|
message = error.json?.message ?? error.reason ?? error.message;
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
message = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: false,
|
||||||
|
code: code,
|
||||||
|
message: message,
|
||||||
|
responseTime,
|
||||||
|
payload: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: response?.State?.Status === "running",
|
||||||
|
code: 200,
|
||||||
|
message: "Docker container status fetched successfully",
|
||||||
|
responseTime,
|
||||||
|
payload: response as unknown as DockerStatusPayload,
|
||||||
|
};
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw new AppError({
|
||||||
|
message: err instanceof Error ? err.message : "Error performing Docker request",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "handle",
|
||||||
|
details: { url: containerInput },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||||
|
import { GameStatusPayload, MonitorStatusResponse } from "@/types/network.js";
|
||||||
|
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||||
|
import { GameDig } from "gamedig";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
|
type GameDigType = typeof GameDig;
|
||||||
|
|
||||||
|
const SERVICE_NAME = "GameProvider";
|
||||||
|
export class GameProvider implements IStatusProvider<GameStatusPayload> {
|
||||||
|
readonly type = "game";
|
||||||
|
constructor(
|
||||||
|
private logger: ILogger,
|
||||||
|
private gameDig: GameDigType
|
||||||
|
) {}
|
||||||
|
|
||||||
|
supports(type: MonitorType): boolean {
|
||||||
|
return type === "game";
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(monitor: Monitor): Promise<MonitorStatusResponse<GameStatusPayload>> {
|
||||||
|
const { url, port, gameId, id, teamId, type } = monitor;
|
||||||
|
|
||||||
|
// Clean the host in case a full URL was passed
|
||||||
|
const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0] || "";
|
||||||
|
try {
|
||||||
|
const state = await this.gameDig
|
||||||
|
.query({
|
||||||
|
type: gameId ?? "unknown",
|
||||||
|
host: host,
|
||||||
|
port: port ?? 0,
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
this.logger.warn({
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "handle",
|
||||||
|
details: { host, port, gameId },
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!state) {
|
||||||
|
return {
|
||||||
|
monitorId: id,
|
||||||
|
teamId: teamId,
|
||||||
|
type: type,
|
||||||
|
status: false,
|
||||||
|
code: NETWORK_ERROR,
|
||||||
|
message: "No response from game server",
|
||||||
|
responseTime: 0,
|
||||||
|
payload: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorId: id,
|
||||||
|
teamId: teamId,
|
||||||
|
type: type,
|
||||||
|
status: true,
|
||||||
|
code: 200,
|
||||||
|
message: "Success",
|
||||||
|
responseTime: state.ping ?? 0,
|
||||||
|
payload: state,
|
||||||
|
};
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new AppError({
|
||||||
|
message: originalMessage || "Error performing game server check",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "handle",
|
||||||
|
details: { url: monitor.url, port: monitor.port, gameId: monitor.gameId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||||
|
import { GrpcStatusPayload, MonitorStatusResponse } from "@/types/network.js";
|
||||||
|
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import { timeRequest } from "@/service/infrastructure/network/utils.js";
|
||||||
|
import * as grpc from "@grpc/grpc-js";
|
||||||
|
import * as protoLoader from "@grpc/proto-loader";
|
||||||
|
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
type GrpcType = typeof grpc;
|
||||||
|
type ProtoLoaderType = typeof protoLoader;
|
||||||
|
|
||||||
|
const SERVICE_NAME = "GrpcProvider";
|
||||||
|
|
||||||
|
export class GrpcProvider implements IStatusProvider<GrpcStatusPayload> {
|
||||||
|
readonly type = "grpc";
|
||||||
|
constructor(
|
||||||
|
private grpc: GrpcType,
|
||||||
|
private protoLoader: ProtoLoaderType
|
||||||
|
) {}
|
||||||
|
|
||||||
|
supports(type: MonitorType): boolean {
|
||||||
|
return type === "grpc";
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGrpcStatusName(code: number): string {
|
||||||
|
const statusNames: Record<number, string> = {
|
||||||
|
0: "OK",
|
||||||
|
1: "CANCELLED",
|
||||||
|
2: "UNKNOWN",
|
||||||
|
3: "INVALID_ARGUMENT",
|
||||||
|
4: "DEADLINE_EXCEEDED",
|
||||||
|
5: "NOT_FOUND",
|
||||||
|
6: "ALREADY_EXISTS",
|
||||||
|
7: "PERMISSION_DENIED",
|
||||||
|
8: "RESOURCE_EXHAUSTED",
|
||||||
|
9: "FAILED_PRECONDITION",
|
||||||
|
10: "ABORTED",
|
||||||
|
11: "OUT_OF_RANGE",
|
||||||
|
12: "UNIMPLEMENTED",
|
||||||
|
13: "INTERNAL",
|
||||||
|
14: "UNAVAILABLE",
|
||||||
|
15: "DATA_LOSS",
|
||||||
|
16: "UNAUTHENTICATED",
|
||||||
|
};
|
||||||
|
return statusNames[code] || "UNKNOWN";
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(monitor: Monitor): Promise<MonitorStatusResponse<GrpcStatusPayload>> {
|
||||||
|
try {
|
||||||
|
const { id, teamId, type, url, port, ignoreTlsErrors } = monitor;
|
||||||
|
const grpcServiceName = monitor.grpcServiceName || "";
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new AppError({ message: "Monitor host is required", service: SERVICE_NAME, method: "handle" });
|
||||||
|
}
|
||||||
|
if (!port) {
|
||||||
|
throw new AppError({ message: "Monitor port is required", service: SERVICE_NAME, method: "handle" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0];
|
||||||
|
const target = `${host}:${port}`;
|
||||||
|
|
||||||
|
const currentFilePath = fileURLToPath(import.meta.url);
|
||||||
|
const protoPath = path.join(path.dirname(currentFilePath), "protos", "health.proto");
|
||||||
|
const packageDefinition = this.protoLoader.loadSync(protoPath, {
|
||||||
|
keepCase: true,
|
||||||
|
longs: String,
|
||||||
|
enums: String,
|
||||||
|
defaults: true,
|
||||||
|
oneofs: true,
|
||||||
|
});
|
||||||
|
const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as unknown as {
|
||||||
|
grpc: {
|
||||||
|
health: {
|
||||||
|
v1: {
|
||||||
|
Health: new (
|
||||||
|
target: string,
|
||||||
|
credentials: unknown
|
||||||
|
) => {
|
||||||
|
Check: (request: { service: string }, options: { deadline: Date }, callback: (err: unknown, response: unknown) => void) => void;
|
||||||
|
close: () => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const healthService = grpcObject.grpc.health.v1.Health;
|
||||||
|
|
||||||
|
let credentials;
|
||||||
|
if (ignoreTlsErrors) {
|
||||||
|
credentials = this.grpc.credentials.createSsl(null, null, null, {
|
||||||
|
checkServerIdentity: () => undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
credentials = this.grpc.credentials.createInsecure();
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new healthService(target, credentials);
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 10000;
|
||||||
|
const deadline = new Date(Date.now() + TIMEOUT_MS);
|
||||||
|
|
||||||
|
const { response, responseTime, error } = await timeRequest<GrpcStatusPayload>(() => {
|
||||||
|
return new Promise<GrpcStatusPayload>((resolve, reject) => {
|
||||||
|
client.Check({ service: grpcServiceName }, { deadline }, (err: unknown, response: unknown) => {
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
const grpcErr = err as { code?: number; details?: string; message?: string };
|
||||||
|
const payload: GrpcStatusPayload = {
|
||||||
|
grpcStatusCode: grpcErr.code ?? -1,
|
||||||
|
grpcStatusName: this.getGrpcStatusName(grpcErr.code ?? -1),
|
||||||
|
serviceName: grpcServiceName,
|
||||||
|
servingStatus: "UNKNOWN",
|
||||||
|
};
|
||||||
|
const grpcError = new AppError({
|
||||||
|
message: grpcErr.details || grpcErr.message || "gRPC error",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "handle",
|
||||||
|
}) as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
|
||||||
|
grpcError.grpcPayload = payload;
|
||||||
|
grpcError.grpcCode = grpcErr.code;
|
||||||
|
reject(grpcError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = response as { status?: string } | undefined;
|
||||||
|
const servingStatus = resp?.status ?? "UNKNOWN";
|
||||||
|
resolve({
|
||||||
|
grpcStatusCode: 0,
|
||||||
|
grpcStatusName: "OK",
|
||||||
|
serviceName: grpcServiceName,
|
||||||
|
servingStatus,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const grpcError = error as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
|
||||||
|
const payload = grpcError.grpcPayload;
|
||||||
|
return {
|
||||||
|
monitorId: id,
|
||||||
|
teamId: teamId,
|
||||||
|
type: type,
|
||||||
|
status: false,
|
||||||
|
code: grpcError.grpcCode ?? 5000,
|
||||||
|
message: grpcError.message ?? "gRPC health check failed",
|
||||||
|
responseTime,
|
||||||
|
payload: payload ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const grpcPayload = response as GrpcStatusPayload;
|
||||||
|
const isServing = grpcPayload.servingStatus === "SERVING";
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorId: id,
|
||||||
|
teamId: teamId,
|
||||||
|
type: type,
|
||||||
|
status: isServing,
|
||||||
|
code: isServing ? 200 : 5000,
|
||||||
|
message: isServing ? `gRPC service healthy (${grpcPayload.servingStatus})` : `gRPC service unhealthy (${grpcPayload.servingStatus})`,
|
||||||
|
responseTime,
|
||||||
|
payload: grpcPayload,
|
||||||
|
};
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new AppError({
|
||||||
|
message: originalMessage || "Error performing gRPC health check",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "handle",
|
||||||
|
details: { url: monitor.url, port: monitor.port, grpcServiceName: monitor.grpcServiceName },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||||
|
import { HardwareStatusPayload, MonitorStatusResponse } from "@/types/network.js";
|
||||||
|
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||||
|
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
|
export class HardwareProvider implements IStatusProvider<HardwareStatusPayload> {
|
||||||
|
readonly type = "hardware";
|
||||||
|
constructor(private httpProvider: HttpProvider) {}
|
||||||
|
|
||||||
|
supports(type: MonitorType) {
|
||||||
|
return type === "hardware";
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(monitor: Monitor): Promise<MonitorStatusResponse<HardwareStatusPayload>> {
|
||||||
|
const { url } = monitor;
|
||||||
|
try {
|
||||||
|
if (!url) throw new Error("URL is required for Hardware monitor");
|
||||||
|
return await this.httpProvider.handle<HardwareStatusPayload>(monitor);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw new AppError({
|
||||||
|
message: err instanceof Error ? err.message : "Error performing Hardware request",
|
||||||
|
service: "HardwareProvider",
|
||||||
|
method: "handle",
|
||||||
|
details: { url: monitor.url },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { type Got, HTTPError, RequestError } from "got";
|
||||||
|
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
|
||||||
|
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||||
|
import { HttpStatusPayload } from "@/types/network.js";
|
||||||
|
import { MonitorStatusResponse } from "@/types/network.js";
|
||||||
|
import { Agent as HttpsAgent } from "https";
|
||||||
|
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||||
|
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
|
||||||
|
import CacheableLookup from "cacheable-lookup";
|
||||||
|
|
||||||
|
export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||||
|
readonly type = "http";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private got: Got,
|
||||||
|
private advancedMatcher: AdvancedMatcher
|
||||||
|
) {
|
||||||
|
const cacheable = new CacheableLookup({ maxTtl: 300, errorTtl: 30 });
|
||||||
|
this.got = got.extend({
|
||||||
|
dnsCache: cacheable,
|
||||||
|
timeout: {
|
||||||
|
request: 30000,
|
||||||
|
},
|
||||||
|
retry: { limit: 1 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
supports(type: MonitorType) {
|
||||||
|
return type === "http";
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleHttpError<T>(error: unknown, monitor: Monitor): MonitorStatusResponse<T> {
|
||||||
|
if (error instanceof HTTPError || error instanceof RequestError) {
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: false,
|
||||||
|
code: error.response?.statusCode ?? NETWORK_ERROR,
|
||||||
|
message: error.message,
|
||||||
|
responseTime: error.timings?.phases?.total ?? 0,
|
||||||
|
timings: error.timings,
|
||||||
|
payload: null as T,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: false,
|
||||||
|
code: NETWORK_ERROR,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
responseTime: 0,
|
||||||
|
payload: null as T,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle<T>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
|
||||||
|
const { url, secret, jsonPath, ignoreTlsErrors } = monitor;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new Error("URL is required for HTTP monitor");
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: Record<string, unknown> = {
|
||||||
|
headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
options.agent = {
|
||||||
|
https: new HttpsAgent({ rejectUnauthorized: !ignoreTlsErrors }),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.got<string>(url, options);
|
||||||
|
const contentType = response.headers["content-type"] || "";
|
||||||
|
const isJson = contentType.includes("application/json");
|
||||||
|
|
||||||
|
if (jsonPath && !isJson) {
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: false,
|
||||||
|
code: response.statusCode,
|
||||||
|
message: "Response is not JSON",
|
||||||
|
responseTime: response.timings.phases.total ?? 0,
|
||||||
|
timings: response.timings,
|
||||||
|
payload: response.body as unknown as T,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: T;
|
||||||
|
if (isJson) {
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(response.body) as T;
|
||||||
|
} catch {
|
||||||
|
payload = response.body as unknown as T;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
payload = response.body as unknown as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchResult = this.advancedMatcher.validate<T>(payload, monitor);
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: response.ok && matchResult.ok,
|
||||||
|
code: response.statusCode,
|
||||||
|
message: matchResult.ok ? (response.statusMessage ?? "OK") : matchResult.message,
|
||||||
|
responseTime: response.timings.phases.total ?? 0,
|
||||||
|
timings: response.timings,
|
||||||
|
payload,
|
||||||
|
extracted: matchResult.extracted,
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return this.handleHttpError(error, monitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||||
|
import { MonitorStatusResponse } from "@/types/network.js";
|
||||||
|
|
||||||
|
export interface IStatusProvider<T> {
|
||||||
|
type: string;
|
||||||
|
supports: (type: MonitorType) => boolean;
|
||||||
|
handle(monitor: Monitor): Promise<MonitorStatusResponse<T>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||||
|
import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.js";
|
||||||
|
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||||
|
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
|
||||||
|
import { ISettingsService } from "@/service/system/settingsService.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
|
export class PageSpeedProvider implements IStatusProvider<PageSpeedStatusPayload> {
|
||||||
|
readonly type = "pagespeed";
|
||||||
|
constructor(
|
||||||
|
private httpProvider: HttpProvider,
|
||||||
|
private settingsService: ISettingsService,
|
||||||
|
private logger: ILogger
|
||||||
|
) {}
|
||||||
|
|
||||||
|
supports(type: MonitorType) {
|
||||||
|
return type === "pagespeed";
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> {
|
||||||
|
const { url } = monitor;
|
||||||
|
try {
|
||||||
|
if (!url) throw new Error("URL is required for PageSpeed monitor");
|
||||||
|
const dbSettings = await this.settingsService.getDBSettings();
|
||||||
|
const apiKey = dbSettings?.pagespeedApiKey;
|
||||||
|
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(
|
||||||
|
url
|
||||||
|
)}&category=seo&category=accessibility&category=best-practices&category=performance`;
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
pageSpeedUrl += `&key=${apiKey}`;
|
||||||
|
} else {
|
||||||
|
this.logger.warn({
|
||||||
|
message: "PageSpeed API key not found, performance may be throttled",
|
||||||
|
service: "PageSpeedProvider",
|
||||||
|
method: "handle",
|
||||||
|
details: { url },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.httpProvider.handle<PageSpeedStatusPayload>({
|
||||||
|
...monitor,
|
||||||
|
url: pageSpeedUrl,
|
||||||
|
});
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw new AppError({
|
||||||
|
message: err instanceof Error ? err.message : "Error performing PageSpeed request",
|
||||||
|
service: "PageSpeedProvider",
|
||||||
|
method: "handle",
|
||||||
|
details: { url: monitor.url },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { PingStatusPayload } from "@/types/network.js";
|
||||||
|
import { IStatusProvider } from "./IStatusProvider.js";
|
||||||
|
import { MonitorType, Monitor } from "@/types/monitor.js";
|
||||||
|
import { MonitorStatusResponse } from "@/types/network.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import ping from "ping";
|
||||||
|
import { timeRequest } from "@/service/infrastructure/network/utils.js";
|
||||||
|
const SERVICE_NAME = "PingProvider";
|
||||||
|
|
||||||
|
type Ping = typeof ping;
|
||||||
|
|
||||||
|
export class PingProvider implements IStatusProvider<PingStatusPayload> {
|
||||||
|
readonly type = "ping";
|
||||||
|
|
||||||
|
constructor(private ping: Ping) {}
|
||||||
|
|
||||||
|
supports(type: MonitorType): boolean {
|
||||||
|
return type === "ping";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizeHost(url: string): string {
|
||||||
|
return url
|
||||||
|
.replace(/^https?:\/\//, "")
|
||||||
|
.replace(/\/.*$/, "")
|
||||||
|
.replace(/:.*/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PingStatusPayload>> {
|
||||||
|
try {
|
||||||
|
if (!monitor.url) {
|
||||||
|
throw new Error("URL is required for ping monitor");
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedHost = this.sanitizeHost(monitor.url);
|
||||||
|
const { response, error } = await timeRequest<PingStatusPayload>(() => this.ping.promise.probe(sanitizedHost));
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw new Error(`No response from ping for host: ${sanitizedHost}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseTime = typeof response.time === "number" ? response.time : parseFloat(String(response.time)) || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: response.alive ?? false,
|
||||||
|
code: response.alive ? 200 : 5000,
|
||||||
|
message: response.alive ? "Success" : "Ping failed",
|
||||||
|
responseTime,
|
||||||
|
payload: response,
|
||||||
|
};
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new AppError({
|
||||||
|
message,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "handle",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||||
|
import { PortStatusPayload, MonitorStatusResponse } from "@/types/network.js";
|
||||||
|
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js";
|
||||||
|
import * as net from "net";
|
||||||
|
type NetType = typeof net;
|
||||||
|
|
||||||
|
const SERVICE_NAME = "PortProvider";
|
||||||
|
|
||||||
|
export class PortProvider implements IStatusProvider<PortStatusPayload> {
|
||||||
|
readonly type = "port";
|
||||||
|
|
||||||
|
constructor(private net: NetType) {}
|
||||||
|
|
||||||
|
supports(type: MonitorType): boolean {
|
||||||
|
return type === "port";
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PortStatusPayload>> {
|
||||||
|
try {
|
||||||
|
const { url, port } = monitor;
|
||||||
|
if (!url || !port) {
|
||||||
|
throw new Error("URL and port are required for port monitoring");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { responseTime, error } = await timeRequest(async () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const socket = this.net.createConnection(
|
||||||
|
{
|
||||||
|
host: url,
|
||||||
|
port,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
socket.end();
|
||||||
|
socket.destroy();
|
||||||
|
resolve({ success: true });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.setTimeout(5000);
|
||||||
|
socket.on("timeout", () => {
|
||||||
|
socket.destroy();
|
||||||
|
reject(new Error("Connection timeout"));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("error", (err: unknown) => {
|
||||||
|
socket.destroy();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Port check failed";
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: false,
|
||||||
|
code: NETWORK_ERROR,
|
||||||
|
message: errorMessage,
|
||||||
|
responseTime: responseTime,
|
||||||
|
timings: undefined,
|
||||||
|
payload: { success: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: true,
|
||||||
|
code: 200,
|
||||||
|
message: "Port check successful",
|
||||||
|
responseTime: responseTime,
|
||||||
|
timings: undefined,
|
||||||
|
payload: { success: true },
|
||||||
|
};
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new AppError({
|
||||||
|
message: originalMessage || "Error performing port check",
|
||||||
|
status: 500,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "handle",
|
||||||
|
details: { url: monitor.url, port: monitor.port },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||||
|
import { WebSocketStatusPayload, MonitorStatusResponse } from "@/types/network.js";
|
||||||
|
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js";
|
||||||
|
import type WebSocket from "ws";
|
||||||
|
|
||||||
|
type WebSocketConstructor = typeof WebSocket;
|
||||||
|
|
||||||
|
const SERVICE_NAME = "WebSocketProvider";
|
||||||
|
const TIMEOUT_MS = 10000;
|
||||||
|
|
||||||
|
export class WebSocketProvider implements IStatusProvider<WebSocketStatusPayload> {
|
||||||
|
readonly type = "websocket";
|
||||||
|
|
||||||
|
constructor(private WS: WebSocketConstructor) {}
|
||||||
|
|
||||||
|
supports(type: MonitorType): boolean {
|
||||||
|
return type === "websocket";
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(monitor: Monitor): Promise<MonitorStatusResponse<WebSocketStatusPayload>> {
|
||||||
|
try {
|
||||||
|
const { url } = monitor;
|
||||||
|
if (!url) {
|
||||||
|
throw new Error("URL is required for WebSocket monitoring");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { responseTime, error } = await timeRequest(async () => {
|
||||||
|
return new Promise<{ connected: boolean }>((resolve, reject) => {
|
||||||
|
const options: WebSocket.ClientOptions = {};
|
||||||
|
if (monitor.ignoreTlsErrors) {
|
||||||
|
options.rejectUnauthorized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = new this.WS(url, options);
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
ws.close();
|
||||||
|
reject(new Error("WebSocket connection timeout"));
|
||||||
|
}, TIMEOUT_MS);
|
||||||
|
|
||||||
|
ws.on("open", () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
ws.close();
|
||||||
|
resolve({ connected: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("error", (err: unknown) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
ws.close();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "WebSocket check failed";
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: false,
|
||||||
|
code: NETWORK_ERROR,
|
||||||
|
message: errorMessage,
|
||||||
|
responseTime: responseTime,
|
||||||
|
timings: undefined,
|
||||||
|
payload: { connected: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: true,
|
||||||
|
code: 200,
|
||||||
|
message: "WebSocket check successful",
|
||||||
|
responseTime: responseTime,
|
||||||
|
timings: undefined,
|
||||||
|
payload: { connected: true },
|
||||||
|
};
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new AppError({
|
||||||
|
message: originalMessage || "Error performing WebSocket check",
|
||||||
|
status: 500,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "handle",
|
||||||
|
details: { url: monitor.url },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ response: T | null; responseTime: number; error: unknown }> => {
|
||||||
|
const start = process.hrtime.bigint();
|
||||||
|
try {
|
||||||
|
const response = await operation();
|
||||||
|
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
|
||||||
|
return { response, responseTime: elapsedMs, error: null };
|
||||||
|
} catch (error) {
|
||||||
|
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
|
||||||
|
return { response: null, responseTime: elapsedMs, error };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NETWORK_ERROR = 5000;
|
||||||
|
export const PING_ERROR = 5001;
|
||||||
@@ -1,57 +1,14 @@
|
|||||||
import { HTTPError, RequestError } from "got";
|
import type { Monitor, MonitorStatusResponse, MonitorPayloadMap, MonitorType } from "@/types/index.js";
|
||||||
import type { Got, Response } from "got";
|
|
||||||
import type {
|
|
||||||
Monitor,
|
|
||||||
MonitorStatusResponse,
|
|
||||||
GrpcStatusPayload,
|
|
||||||
PageSpeedStatusPayload,
|
|
||||||
HttpStatusPayload,
|
|
||||||
HardwareStatusPayload,
|
|
||||||
PingStatusPayload,
|
|
||||||
DockerStatusPayload,
|
|
||||||
PortStatusPayload,
|
|
||||||
GameStatusPayload,
|
|
||||||
} from "@/types/index.js";
|
|
||||||
import type { AxiosStatic } from "axios";
|
import type { AxiosStatic } from "axios";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import path from "path";
|
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
import CacheableLookup from "cacheable-lookup";
|
|
||||||
import { ISettingsService } from "../system/settingsService.js";
|
|
||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
import { IStatusProvider } from "./network/IStatusProvider.js";
|
||||||
const SERVICE_NAME = "NetworkService";
|
const SERVICE_NAME = "NetworkService";
|
||||||
|
|
||||||
type MonitorStatusResponseOverrides<T> = Partial<Omit<MonitorStatusResponse<T>, "monitorId" | "teamId" | "type">>;
|
|
||||||
|
|
||||||
interface BuildStatusResponseArgs<T> {
|
|
||||||
monitor: Monitor;
|
|
||||||
response?: Response<T> | null;
|
|
||||||
error?: Error | RequestError | HTTPError | null;
|
|
||||||
payload?: T | null;
|
|
||||||
jsonPath?: string;
|
|
||||||
matchMethod?: MonitorStatusResponse["matchMethod"];
|
|
||||||
expectedValue?: string;
|
|
||||||
extracted?: unknown;
|
|
||||||
overrides?: MonitorStatusResponseOverrides<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INetworkService {
|
export interface INetworkService {
|
||||||
readonly serviceName: string;
|
readonly serviceName: string;
|
||||||
requestStatus(
|
requestStatus<T extends MonitorType>(monitor: Monitor & { type: T }): Promise<MonitorStatusResponse<MonitorPayloadMap[T]>>;
|
||||||
monitor: Monitor
|
|
||||||
): Promise<
|
|
||||||
MonitorStatusResponse<
|
|
||||||
| PingStatusPayload
|
|
||||||
| HttpStatusPayload
|
|
||||||
| PageSpeedStatusPayload
|
|
||||||
| HardwareStatusPayload
|
|
||||||
| DockerStatusPayload
|
|
||||||
| PortStatusPayload
|
|
||||||
| GameStatusPayload
|
|
||||||
| GrpcStatusPayload
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
requestWebhook(
|
requestWebhook(
|
||||||
type: string,
|
type: string,
|
||||||
url: string,
|
url: string,
|
||||||
@@ -66,791 +23,32 @@ export interface INetworkService {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NetworkService implements INetworkService {
|
export class NetworkService implements INetworkService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private TYPE_PING: string;
|
|
||||||
private TYPE_HTTP: string;
|
|
||||||
private TYPE_PAGESPEED: string;
|
|
||||||
private TYPE_HARDWARE: string;
|
|
||||||
private TYPE_DOCKER: string;
|
|
||||||
private TYPE_PORT: string;
|
|
||||||
private TYPE_GAME: string;
|
|
||||||
private TYPE_GRPC: string;
|
|
||||||
private SERVICE_NAME: string;
|
|
||||||
private NETWORK_ERROR: number;
|
|
||||||
private PING_ERROR: number;
|
|
||||||
|
|
||||||
private axios: AxiosStatic;
|
private axios: AxiosStatic;
|
||||||
private got: Got;
|
|
||||||
private https: typeof import("https");
|
|
||||||
private jmespath: { search: (data: unknown, expression: string) => unknown };
|
|
||||||
private GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> };
|
|
||||||
private ping: typeof import("ping");
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
private Docker: typeof import("dockerode");
|
|
||||||
private net: typeof import("net");
|
|
||||||
private settingsService: ISettingsService;
|
|
||||||
private grpc: typeof import("@grpc/grpc-js");
|
|
||||||
private protoLoader: typeof import("@grpc/proto-loader");
|
|
||||||
|
|
||||||
private buildStatusResponse = <T>({
|
|
||||||
monitor,
|
|
||||||
response,
|
|
||||||
error,
|
|
||||||
payload,
|
|
||||||
jsonPath,
|
|
||||||
matchMethod,
|
|
||||||
expectedValue,
|
|
||||||
extracted,
|
|
||||||
overrides,
|
|
||||||
}: BuildStatusResponseArgs<T>): MonitorStatusResponse<T> => {
|
|
||||||
if (error) {
|
|
||||||
const statusResponse: MonitorStatusResponse<T> = {
|
|
||||||
monitorId: monitor.id,
|
|
||||||
teamId: monitor.teamId,
|
|
||||||
type: monitor.type,
|
|
||||||
status: false,
|
|
||||||
code: this.NETWORK_ERROR,
|
|
||||||
message: error.message ?? "Network error",
|
|
||||||
responseTime: 0,
|
|
||||||
timings: undefined,
|
|
||||||
jsonPath,
|
|
||||||
matchMethod,
|
|
||||||
expectedValue,
|
|
||||||
extracted,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
if (error instanceof HTTPError || error instanceof RequestError) {
|
|
||||||
statusResponse.code = error?.response?.statusCode ?? this.NETWORK_ERROR;
|
|
||||||
statusResponse.message = error.message;
|
|
||||||
statusResponse.responseTime = error.timings?.phases?.total ?? 0;
|
|
||||||
statusResponse.timings = error.timings;
|
|
||||||
}
|
|
||||||
return { ...statusResponse, ...(overrides ?? {}) } as MonitorStatusResponse<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
monitorId: monitor.id,
|
|
||||||
teamId: monitor.teamId,
|
|
||||||
type: monitor.type,
|
|
||||||
status: response?.ok ?? false,
|
|
||||||
code: response?.statusCode ?? this.NETWORK_ERROR,
|
|
||||||
message: response?.statusMessage ?? "",
|
|
||||||
responseTime: response?.timings?.phases?.total ?? 0,
|
|
||||||
timings: response?.timings,
|
|
||||||
payload: payload ?? response?.body,
|
|
||||||
jsonPath,
|
|
||||||
matchMethod,
|
|
||||||
expectedValue,
|
|
||||||
extracted,
|
|
||||||
...(overrides ?? {}),
|
|
||||||
} as MonitorStatusResponse<T>;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
axios: AxiosStatic,
|
axios: AxiosStatic,
|
||||||
got: Got,
|
|
||||||
https: typeof import("https"),
|
|
||||||
jmespath: { search: (data: unknown, expression: string) => unknown },
|
|
||||||
GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> },
|
|
||||||
ping: typeof import("ping"),
|
|
||||||
logger: ILogger,
|
logger: ILogger,
|
||||||
Docker: typeof import("dockerode"),
|
private providers: IStatusProvider<unknown>[]
|
||||||
net: typeof import("net"),
|
|
||||||
settingsService: ISettingsService,
|
|
||||||
grpc: typeof import("@grpc/grpc-js"),
|
|
||||||
protoLoader: typeof import("@grpc/proto-loader")
|
|
||||||
) {
|
) {
|
||||||
this.TYPE_PING = "ping";
|
|
||||||
this.TYPE_HTTP = "http";
|
|
||||||
this.TYPE_PAGESPEED = "pagespeed";
|
|
||||||
this.TYPE_HARDWARE = "hardware";
|
|
||||||
this.TYPE_DOCKER = "docker";
|
|
||||||
this.TYPE_PORT = "port";
|
|
||||||
this.TYPE_GAME = "game";
|
|
||||||
this.TYPE_GRPC = "grpc";
|
|
||||||
this.SERVICE_NAME = SERVICE_NAME;
|
|
||||||
this.NETWORK_ERROR = 5000;
|
|
||||||
this.PING_ERROR = 5001;
|
|
||||||
this.axios = axios;
|
this.axios = axios;
|
||||||
this.https = https;
|
|
||||||
this.jmespath = jmespath;
|
|
||||||
this.GameDig = GameDig;
|
|
||||||
this.ping = ping;
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.Docker = Docker;
|
|
||||||
this.net = net;
|
|
||||||
this.settingsService = settingsService;
|
|
||||||
this.grpc = grpc;
|
|
||||||
this.protoLoader = protoLoader;
|
|
||||||
|
|
||||||
const cacheable = new CacheableLookup();
|
|
||||||
|
|
||||||
this.got = got.extend({
|
|
||||||
dnsCache: cacheable,
|
|
||||||
timeout: {
|
|
||||||
request: 30000,
|
|
||||||
},
|
|
||||||
retry: { limit: 1 },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get serviceName(): string {
|
get serviceName(): string {
|
||||||
return NetworkService.SERVICE_NAME;
|
return NetworkService.SERVICE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
private async timeRequest<T>(operation: () => Promise<T>): Promise<{ response: T | null; responseTime: number; error: unknown }> {
|
|
||||||
const start = process.hrtime.bigint();
|
|
||||||
try {
|
|
||||||
const response = await operation();
|
|
||||||
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
|
|
||||||
return { response, responseTime: elapsedMs, error: null };
|
|
||||||
} catch (error) {
|
|
||||||
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
|
|
||||||
return { response: null, responseTime: elapsedMs, error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main entry point
|
// Main entry point
|
||||||
async requestStatus(
|
async requestStatus<T extends MonitorType>(monitor: Monitor & { type: T }): Promise<MonitorStatusResponse<MonitorPayloadMap[T]>> {
|
||||||
monitor: Monitor
|
const provider = this.providers.find((p) => p.supports(monitor.type));
|
||||||
): Promise<
|
if (!provider) {
|
||||||
MonitorStatusResponse<
|
return this.handleUnsupportedType(monitor.type) as Promise<MonitorStatusResponse<MonitorPayloadMap[T]>>;
|
||||||
| PingStatusPayload
|
|
||||||
| HttpStatusPayload
|
|
||||||
| PageSpeedStatusPayload
|
|
||||||
| HardwareStatusPayload
|
|
||||||
| DockerStatusPayload
|
|
||||||
| PortStatusPayload
|
|
||||||
| GameStatusPayload
|
|
||||||
| GrpcStatusPayload
|
|
||||||
>
|
|
||||||
> {
|
|
||||||
const type = monitor?.type || "unknown";
|
|
||||||
switch (type) {
|
|
||||||
case this.TYPE_PING:
|
|
||||||
return await this.requestPing(monitor);
|
|
||||||
case this.TYPE_HTTP:
|
|
||||||
return await this.requestHttp<HttpStatusPayload>(monitor);
|
|
||||||
case this.TYPE_PAGESPEED:
|
|
||||||
return await this.requestPageSpeed(monitor);
|
|
||||||
case this.TYPE_HARDWARE:
|
|
||||||
return await this.requestHardware(monitor);
|
|
||||||
case this.TYPE_DOCKER:
|
|
||||||
return await this.requestDocker(monitor);
|
|
||||||
case this.TYPE_PORT:
|
|
||||||
return await this.requestPort(monitor);
|
|
||||||
case this.TYPE_GAME:
|
|
||||||
return await this.requestGame(monitor);
|
|
||||||
case this.TYPE_GRPC:
|
|
||||||
return await this.requestGrpc(monitor);
|
|
||||||
default:
|
|
||||||
return this.handleUnsupportedType(type);
|
|
||||||
}
|
}
|
||||||
}
|
return provider.handle(monitor) as Promise<MonitorStatusResponse<MonitorPayloadMap[T]>>;
|
||||||
|
|
||||||
private async requestPing(monitor: Monitor): Promise<MonitorStatusResponse<PingStatusPayload>> {
|
|
||||||
try {
|
|
||||||
if (!monitor?.url) {
|
|
||||||
throw new Error("Monitor URL is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawUrl = monitor.url;
|
|
||||||
const sanitizedHost = rawUrl
|
|
||||||
.replace(/^https?:\/\//, "")
|
|
||||||
.replace(/\/.*$/, "")
|
|
||||||
.replace(/:.*/, "");
|
|
||||||
const { response, error } = await this.timeRequest(() => this.ping.promise.probe(sanitizedHost));
|
|
||||||
|
|
||||||
if (!response) {
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error("Ping failed - no result returned");
|
|
||||||
}
|
|
||||||
|
|
||||||
const pingResponse = this.buildStatusResponse<PingStatusPayload>({
|
|
||||||
monitor,
|
|
||||||
payload: response as PingStatusPayload,
|
|
||||||
overrides: {
|
|
||||||
status: (response as { alive?: boolean })?.alive ?? false,
|
|
||||||
code: 200,
|
|
||||||
message: "Success",
|
|
||||||
responseTime: (response as { time?: number })?.time ?? 0,
|
|
||||||
payload: response as PingStatusPayload,
|
|
||||||
} as MonitorStatusResponseOverrides<PingStatusPayload>,
|
|
||||||
});
|
|
||||||
|
|
||||||
return pingResponse;
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
||||||
throw new AppError({
|
|
||||||
message: originalMessage || "Error performing ping",
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestPing",
|
|
||||||
details: { url: monitor.url },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async requestHttp<T = unknown>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
|
|
||||||
const { url, secret, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor;
|
|
||||||
const httpResponse = this.buildStatusResponse<T>({
|
|
||||||
monitor,
|
|
||||||
overrides: {
|
|
||||||
status: false,
|
|
||||||
code: this.NETWORK_ERROR,
|
|
||||||
message: "Request not executed",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!url) {
|
|
||||||
throw new Error("Monitor URL is required");
|
|
||||||
}
|
|
||||||
const config: Record<string, unknown> = {
|
|
||||||
headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ignoreTlsErrors) {
|
|
||||||
config.agent = {
|
|
||||||
https: new this.https.Agent({
|
|
||||||
rejectUnauthorized: false,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.got(url, config);
|
|
||||||
|
|
||||||
let payload: T | string | undefined;
|
|
||||||
const contentType = response.headers["content-type"];
|
|
||||||
|
|
||||||
if (contentType && contentType.includes("application/json")) {
|
|
||||||
try {
|
|
||||||
payload = JSON.parse(response.body);
|
|
||||||
} catch {
|
|
||||||
payload = response.body;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
payload = response.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(httpResponse, {
|
|
||||||
code: response.statusCode,
|
|
||||||
status: response.ok,
|
|
||||||
message: response.statusMessage ?? "",
|
|
||||||
responseTime: response.timings.phases.total || 0,
|
|
||||||
payload,
|
|
||||||
timings: response.timings,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!useAdvancedMatching) {
|
|
||||||
return httpResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expectedValue && !jsonPath) {
|
|
||||||
let ok = false;
|
|
||||||
if (matchMethod === "equal") ok = payload === expectedValue;
|
|
||||||
if (matchMethod === "include" && typeof payload === "string") ok = payload.includes(expectedValue);
|
|
||||||
if (matchMethod === "regex" && typeof payload === "string") ok = new RegExp(expectedValue).test(payload);
|
|
||||||
|
|
||||||
if (ok === true) {
|
|
||||||
return httpResponse;
|
|
||||||
} else {
|
|
||||||
httpResponse.code = 500;
|
|
||||||
httpResponse.status = false;
|
|
||||||
httpResponse.message = "Expected value did not match";
|
|
||||||
return httpResponse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonPath) {
|
|
||||||
const contentType = response.headers["content-type"];
|
|
||||||
const isJson = contentType?.includes("application/json");
|
|
||||||
if (!isJson) {
|
|
||||||
httpResponse.status = false;
|
|
||||||
httpResponse.message = "Response is not JSON";
|
|
||||||
return httpResponse;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const extracted = this.jmespath.search(payload, jsonPath);
|
|
||||||
if (expectedValue) {
|
|
||||||
let ok = false;
|
|
||||||
if (matchMethod === "equal") ok = extracted === expectedValue;
|
|
||||||
if (matchMethod === "include" && typeof extracted === "string") ok = extracted.includes(expectedValue);
|
|
||||||
if (matchMethod === "regex" && typeof extracted === "string") ok = new RegExp(expectedValue).test(extracted);
|
|
||||||
|
|
||||||
if (ok) {
|
|
||||||
httpResponse.extracted = extracted;
|
|
||||||
return httpResponse;
|
|
||||||
} else {
|
|
||||||
httpResponse.status = false;
|
|
||||||
httpResponse.code = 500;
|
|
||||||
httpResponse.message = "Expected value did not match";
|
|
||||||
httpResponse.extracted = extracted;
|
|
||||||
return httpResponse;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const isFalsey = extracted === false || extracted === "false" || extracted === undefined || extracted === null;
|
|
||||||
if (!isFalsey) {
|
|
||||||
httpResponse.extracted = extracted;
|
|
||||||
return httpResponse;
|
|
||||||
} else {
|
|
||||||
httpResponse.status = false;
|
|
||||||
httpResponse.code = 500;
|
|
||||||
httpResponse.message = "Expected value did not match";
|
|
||||||
httpResponse.extracted = extracted;
|
|
||||||
return httpResponse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
httpResponse.status = false;
|
|
||||||
httpResponse.message = "Error evaluating JSON path";
|
|
||||||
return httpResponse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return httpResponse;
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (err instanceof HTTPError || err instanceof RequestError) {
|
|
||||||
httpResponse.code = err.response?.statusCode || this.NETWORK_ERROR;
|
|
||||||
httpResponse.status = false;
|
|
||||||
httpResponse.message = String(err.response?.statusCode) || err.message;
|
|
||||||
httpResponse.responseTime = err.timings?.phases?.total || 0;
|
|
||||||
httpResponse.payload = null;
|
|
||||||
httpResponse.timings = err.timings || undefined;
|
|
||||||
return httpResponse;
|
|
||||||
}
|
|
||||||
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
||||||
throw new AppError({
|
|
||||||
message: originalMessage || "Error performing HTTP request",
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestHttp",
|
|
||||||
details: { url: monitor.url },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async requestPageSpeed(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> {
|
|
||||||
try {
|
|
||||||
const url = monitor.url;
|
|
||||||
if (!url) {
|
|
||||||
throw new Error("Monitor URL is required");
|
|
||||||
}
|
|
||||||
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
|
|
||||||
const dbSettings = await this.settingsService.getDBSettings();
|
|
||||||
if (dbSettings?.pagespeedApiKey) {
|
|
||||||
pageSpeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
|
|
||||||
} else {
|
|
||||||
this.logger.warn({
|
|
||||||
message: "PageSpeed API key not found, job not executed",
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestPagespeed",
|
|
||||||
details: { url },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await this.requestHttp<PageSpeedStatusPayload>({
|
|
||||||
...monitor,
|
|
||||||
url: pageSpeedUrl,
|
|
||||||
});
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
||||||
throw new AppError({
|
|
||||||
message: originalMessage || "Error performing PageSpeed request",
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestPageSpeed",
|
|
||||||
details: { url: monitor.url },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async requestHardware(monitor: Monitor): Promise<MonitorStatusResponse<HardwareStatusPayload>> {
|
|
||||||
try {
|
|
||||||
return await this.requestHttp<HardwareStatusPayload>(monitor);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
||||||
throw new AppError({
|
|
||||||
message: originalMessage || "Error performing hardware request",
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestHardware",
|
|
||||||
details: { url: monitor.url },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async requestDocker(monitor: Monitor): Promise<MonitorStatusResponse<DockerStatusPayload>> {
|
|
||||||
try {
|
|
||||||
if (!monitor.url) {
|
|
||||||
throw new Error("Monitor URL is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const docker = new this.Docker({
|
|
||||||
socketPath: "/var/run/docker.sock",
|
|
||||||
});
|
|
||||||
|
|
||||||
const dockerResponse = this.buildStatusResponse<DockerStatusPayload>({
|
|
||||||
monitor,
|
|
||||||
overrides: {
|
|
||||||
status: false,
|
|
||||||
code: this.NETWORK_ERROR,
|
|
||||||
message: "No response",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const containers = await docker.listContainers({ all: true });
|
|
||||||
|
|
||||||
// Normalize input: strip leading slashes and convert to lowercase for comparison
|
|
||||||
const normalizedInput = monitor.url.replace(/^\/+/, "").toLowerCase();
|
|
||||||
|
|
||||||
// Priority-based matching to avoid ambiguity:
|
|
||||||
// 1. Exact full ID match (64-char)
|
|
||||||
const exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput);
|
|
||||||
|
|
||||||
// 2. Exact container name match (case-insensitive)
|
|
||||||
const exactNameMatch = containers.find((c) =>
|
|
||||||
c.Names.some((name: string) => {
|
|
||||||
const cleanName = name.replace(/^\/+/, "").toLowerCase();
|
|
||||||
return cleanName === normalizedInput;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Partial ID match (fallback for backwards compatibility)
|
|
||||||
const partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput));
|
|
||||||
|
|
||||||
// Select container based on priority
|
|
||||||
const targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
|
|
||||||
|
|
||||||
// Return negative response if no container
|
|
||||||
if (!targetContainer) {
|
|
||||||
this.logger.warn({
|
|
||||||
message: `No container found for "${monitor.url}".`,
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestDocker",
|
|
||||||
details: { url: monitor.url },
|
|
||||||
});
|
|
||||||
|
|
||||||
dockerResponse.code = 404;
|
|
||||||
dockerResponse.status = false;
|
|
||||||
dockerResponse.message = "Docker container not found";
|
|
||||||
return dockerResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return negative response if ambiguous matches exist
|
|
||||||
const matchTypes = [];
|
|
||||||
if (exactIdMatch) matchTypes.push("exact ID");
|
|
||||||
if (exactNameMatch) matchTypes.push("exact name");
|
|
||||||
if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID");
|
|
||||||
|
|
||||||
if (matchTypes.length > 1) {
|
|
||||||
this.logger.warn({
|
|
||||||
message: `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`,
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestDocker",
|
|
||||||
details: { url: monitor.url },
|
|
||||||
});
|
|
||||||
dockerResponse.status = false;
|
|
||||||
dockerResponse.message = `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`;
|
|
||||||
return dockerResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = docker.getContainer(targetContainer.Id);
|
|
||||||
const { response, responseTime, error }: { response?: { State: { Status: string } } | null; responseTime: number; error: unknown } =
|
|
||||||
await this.timeRequest(() => container.inspect());
|
|
||||||
|
|
||||||
dockerResponse.responseTime = responseTime;
|
|
||||||
dockerResponse.status = response?.State?.Status === "running" ? true : false;
|
|
||||||
dockerResponse.code = 200;
|
|
||||||
dockerResponse.message = "Docker container status fetched successfully";
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
dockerResponse.status = false;
|
|
||||||
dockerResponse.code =
|
|
||||||
(error && typeof error === "object" && "statusCode" in error ? (error as { statusCode?: number }).statusCode : undefined) ||
|
|
||||||
this.NETWORK_ERROR;
|
|
||||||
dockerResponse.message =
|
|
||||||
(error && typeof error === "object" && "reason" in error ? (error as { reason?: string }).reason : undefined) ||
|
|
||||||
"Failed to fetch Docker container information";
|
|
||||||
return dockerResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dockerResponse;
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
||||||
throw new AppError({
|
|
||||||
message: originalMessage || "Error performing Docker request",
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestDocker",
|
|
||||||
details: { url: monitor.url },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async requestPort(monitor: Monitor): Promise<MonitorStatusResponse<PortStatusPayload>> {
|
|
||||||
try {
|
|
||||||
const { url, port } = monitor;
|
|
||||||
if (!port) {
|
|
||||||
throw new Error("Port is required for port monitor");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { response, responseTime, error } = await this.timeRequest(async () => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const socket = this.net.createConnection(
|
|
||||||
{
|
|
||||||
host: url,
|
|
||||||
port,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
socket.end();
|
|
||||||
socket.destroy();
|
|
||||||
resolve({ success: true });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
socket.setTimeout(5000);
|
|
||||||
socket.on("timeout", () => {
|
|
||||||
socket.destroy();
|
|
||||||
reject(new Error("Connection timeout"));
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("error", (err: unknown) => {
|
|
||||||
socket.destroy();
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const portResponse = this.buildStatusResponse<PortStatusPayload>({
|
|
||||||
monitor,
|
|
||||||
overrides: {
|
|
||||||
code: 200,
|
|
||||||
status: (response as { success?: boolean })?.success ?? false,
|
|
||||||
message: "Port check successful",
|
|
||||||
responseTime,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
portResponse.code = this.NETWORK_ERROR;
|
|
||||||
portResponse.status = false;
|
|
||||||
portResponse.message = "Port check failed";
|
|
||||||
return portResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
return portResponse;
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
||||||
throw new AppError({
|
|
||||||
message: originalMessage || "Error performing port check",
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestPort",
|
|
||||||
details: { url: monitor.url, port: monitor.port },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async requestGame(monitor: Monitor): Promise<MonitorStatusResponse<GameStatusPayload>> {
|
|
||||||
try {
|
|
||||||
const { url, port, gameId } = monitor;
|
|
||||||
|
|
||||||
const gameResponse = this.buildStatusResponse<GameStatusPayload>({
|
|
||||||
monitor,
|
|
||||||
overrides: {
|
|
||||||
code: 200,
|
|
||||||
status: true,
|
|
||||||
message: "Success",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = await this.GameDig.query({
|
|
||||||
type: gameId ?? "unknown",
|
|
||||||
host: url,
|
|
||||||
port: port ?? 0,
|
|
||||||
}).catch((error: unknown): undefined => {
|
|
||||||
this.logger.warn({
|
|
||||||
message: error instanceof Error ? error.message : String(error),
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestGame",
|
|
||||||
details: { url, port, gameId },
|
|
||||||
});
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!state) {
|
|
||||||
gameResponse.code = this.NETWORK_ERROR;
|
|
||||||
gameResponse.status = false;
|
|
||||||
gameResponse.message = "No response";
|
|
||||||
return gameResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
gameResponse.responseTime = state.ping ?? 0;
|
|
||||||
gameResponse.payload = state as GameStatusPayload;
|
|
||||||
return gameResponse;
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
||||||
throw new AppError({
|
|
||||||
message: originalMessage || "Error performing game server check",
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestGame",
|
|
||||||
details: { url: monitor.url, port: monitor.port, gameId: monitor.gameId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private async requestGrpc(monitor: Monitor): Promise<MonitorStatusResponse<GrpcStatusPayload>> {
|
|
||||||
try {
|
|
||||||
const { url, port, ignoreTlsErrors } = monitor;
|
|
||||||
const grpcServiceName = monitor.grpcServiceName || "";
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
throw new AppError({ message: "Monitor host is required", service: this.SERVICE_NAME, method: "requestGrpc" });
|
|
||||||
}
|
|
||||||
if (!port) {
|
|
||||||
throw new AppError({ message: "Monitor port is required", service: this.SERVICE_NAME, method: "requestGrpc" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = `${url}:${port}`;
|
|
||||||
|
|
||||||
const currentFilePath = fileURLToPath(import.meta.url);
|
|
||||||
const protoPath = path.join(path.dirname(currentFilePath), "protos", "health.proto");
|
|
||||||
|
|
||||||
const packageDefinition = this.protoLoader.loadSync(protoPath, {
|
|
||||||
keepCase: true,
|
|
||||||
longs: String,
|
|
||||||
enums: String,
|
|
||||||
defaults: true,
|
|
||||||
oneofs: true,
|
|
||||||
});
|
|
||||||
const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as unknown as {
|
|
||||||
grpc: {
|
|
||||||
health: {
|
|
||||||
v1: {
|
|
||||||
Health: new (
|
|
||||||
target: string,
|
|
||||||
credentials: unknown
|
|
||||||
) => {
|
|
||||||
Check: (request: { service: string }, options: { deadline: Date }, callback: (err: unknown, response: unknown) => void) => void;
|
|
||||||
close: () => void;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const healthService = grpcObject.grpc.health.v1.Health;
|
|
||||||
|
|
||||||
let credentials;
|
|
||||||
if (ignoreTlsErrors) {
|
|
||||||
credentials = this.grpc.credentials.createSsl(null, null, null, {
|
|
||||||
checkServerIdentity: () => undefined,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
credentials = this.grpc.credentials.createInsecure();
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new healthService(target, credentials);
|
|
||||||
|
|
||||||
const TIMEOUT_MS = 10000;
|
|
||||||
const deadline = new Date(Date.now() + TIMEOUT_MS);
|
|
||||||
|
|
||||||
const grpcResponse = this.buildStatusResponse<GrpcStatusPayload>({
|
|
||||||
monitor,
|
|
||||||
overrides: {
|
|
||||||
status: false,
|
|
||||||
code: this.NETWORK_ERROR,
|
|
||||||
message: "gRPC health check not executed",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { response, responseTime, error } = await this.timeRequest<GrpcStatusPayload>(() => {
|
|
||||||
return new Promise<GrpcStatusPayload>((resolve, reject) => {
|
|
||||||
client.Check({ service: grpcServiceName }, { deadline }, (err: unknown, response: unknown) => {
|
|
||||||
client.close();
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
const grpcErr = err as { code?: number; details?: string; message?: string };
|
|
||||||
const payload: GrpcStatusPayload = {
|
|
||||||
grpcStatusCode: grpcErr.code ?? -1,
|
|
||||||
grpcStatusName: this.getGrpcStatusName(grpcErr.code ?? -1),
|
|
||||||
serviceName: grpcServiceName,
|
|
||||||
servingStatus: "UNKNOWN",
|
|
||||||
};
|
|
||||||
const grpcError = new AppError({
|
|
||||||
message: grpcErr.details || grpcErr.message || "gRPC error",
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestGrpc",
|
|
||||||
}) as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
|
|
||||||
grpcError.grpcPayload = payload;
|
|
||||||
grpcError.grpcCode = grpcErr.code;
|
|
||||||
reject(grpcError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = response as { status?: string } | undefined;
|
|
||||||
const servingStatus = resp?.status ?? "UNKNOWN";
|
|
||||||
resolve({
|
|
||||||
grpcStatusCode: 0,
|
|
||||||
grpcStatusName: "OK",
|
|
||||||
serviceName: grpcServiceName,
|
|
||||||
servingStatus,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
const grpcError = error as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
|
|
||||||
const payload = grpcError.grpcPayload;
|
|
||||||
grpcResponse.status = false;
|
|
||||||
grpcResponse.code = grpcError.grpcCode ?? this.NETWORK_ERROR;
|
|
||||||
grpcResponse.message = grpcError.message ?? "gRPC health check failed";
|
|
||||||
grpcResponse.responseTime = responseTime;
|
|
||||||
grpcResponse.payload = payload ?? null;
|
|
||||||
return grpcResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
const grpcPayload = response as GrpcStatusPayload;
|
|
||||||
const isServing = grpcPayload.servingStatus === "SERVING";
|
|
||||||
|
|
||||||
grpcResponse.status = isServing;
|
|
||||||
grpcResponse.code = isServing ? 200 : this.NETWORK_ERROR;
|
|
||||||
grpcResponse.message = isServing
|
|
||||||
? `gRPC service healthy (${grpcPayload.servingStatus})`
|
|
||||||
: `gRPC service unhealthy (${grpcPayload.servingStatus})`;
|
|
||||||
grpcResponse.responseTime = responseTime;
|
|
||||||
grpcResponse.payload = grpcPayload;
|
|
||||||
|
|
||||||
return grpcResponse;
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
||||||
throw new AppError({
|
|
||||||
message: originalMessage || "Error performing gRPC health check",
|
|
||||||
service: this.SERVICE_NAME,
|
|
||||||
method: "requestGrpc",
|
|
||||||
details: { url: monitor.url, port: monitor.port, grpcServiceName: monitor.grpcServiceName },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getGrpcStatusName(code: number): string {
|
|
||||||
const statusNames: Record<number, string> = {
|
|
||||||
0: "OK",
|
|
||||||
1: "CANCELLED",
|
|
||||||
2: "UNKNOWN",
|
|
||||||
3: "INVALID_ARGUMENT",
|
|
||||||
4: "DEADLINE_EXCEEDED",
|
|
||||||
5: "NOT_FOUND",
|
|
||||||
6: "ALREADY_EXISTS",
|
|
||||||
7: "PERMISSION_DENIED",
|
|
||||||
8: "RESOURCE_EXHAUSTED",
|
|
||||||
9: "FAILED_PRECONDITION",
|
|
||||||
10: "ABORTED",
|
|
||||||
11: "OUT_OF_RANGE",
|
|
||||||
12: "UNIMPLEMENTED",
|
|
||||||
13: "INTERNAL",
|
|
||||||
14: "UNAVAILABLE",
|
|
||||||
15: "DATA_LOSS",
|
|
||||||
16: "UNAUTHENTICATED",
|
|
||||||
};
|
|
||||||
return statusNames[code] || "UNKNOWN";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
|
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
|
||||||
@@ -859,7 +57,7 @@ class NetworkService implements INetworkService {
|
|||||||
teamId: "unknown",
|
teamId: "unknown",
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
status: false,
|
status: false,
|
||||||
code: this.NETWORK_ERROR,
|
code: NETWORK_ERROR,
|
||||||
message: `Unsupported type: ${type}`,
|
message: `Unsupported type: ${type}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -883,7 +81,7 @@ class NetworkService implements INetworkService {
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: err instanceof Error ? err.message : String(err),
|
message: err instanceof Error ? err.message : String(err),
|
||||||
service: this.SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "requestWebhook",
|
method: "requestWebhook",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -892,7 +90,7 @@ class NetworkService implements INetworkService {
|
|||||||
return {
|
return {
|
||||||
type: "webhook",
|
type: "webhook",
|
||||||
status: false,
|
status: false,
|
||||||
code: axiosError.response?.status ?? this.NETWORK_ERROR,
|
code: axiosError.response?.status ?? NETWORK_ERROR,
|
||||||
message: `Failed to send ${type} notification`,
|
message: `Failed to send ${type} notification`,
|
||||||
payload: axiosError.response?.data,
|
payload: axiosError.response?.data,
|
||||||
};
|
};
|
||||||
@@ -901,7 +99,7 @@ class NetworkService implements INetworkService {
|
|||||||
return {
|
return {
|
||||||
type: "webhook",
|
type: "webhook",
|
||||||
status: false,
|
status: false,
|
||||||
code: this.NETWORK_ERROR,
|
code: NETWORK_ERROR,
|
||||||
message: `Failed to send ${type} notification`,
|
message: `Failed to send ${type} notification`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -927,7 +125,7 @@ class NetworkService implements INetworkService {
|
|||||||
|
|
||||||
throw new AppError({
|
throw new AppError({
|
||||||
message: originalMessage || "Error sending PagerDuty notification",
|
message: originalMessage || "Error sending PagerDuty notification",
|
||||||
service: this.SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "requestPagerDuty",
|
method: "requestPagerDuty",
|
||||||
details: {
|
details: {
|
||||||
responseData: err && typeof err === "object" && "response" in err ? (err as { response?: { data?: unknown } }).response?.data : undefined,
|
responseData: err && typeof err === "object" && "response" in err ? (err as { response?: { data?: unknown } }).response?.data : undefined,
|
||||||
@@ -970,7 +168,7 @@ class NetworkService implements INetworkService {
|
|||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: err.message,
|
message: err.message,
|
||||||
service: this.SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "requestMatrix",
|
method: "requestMatrix",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -978,7 +176,7 @@ class NetworkService implements INetworkService {
|
|||||||
const axiosError = err as { response?: { status?: number; data?: unknown } };
|
const axiosError = err as { response?: { status?: number; data?: unknown } };
|
||||||
return {
|
return {
|
||||||
status: false,
|
status: false,
|
||||||
code: axiosError.response?.status || this.NETWORK_ERROR,
|
code: axiosError.response?.status || NETWORK_ERROR,
|
||||||
message: "Failed to send Matrix notification",
|
message: "Failed to send Matrix notification",
|
||||||
payload: axiosError.response?.data,
|
payload: axiosError.response?.data,
|
||||||
};
|
};
|
||||||
@@ -987,17 +185,15 @@ class NetworkService implements INetworkService {
|
|||||||
|
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: String(err),
|
message: String(err),
|
||||||
service: this.SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "requestMatrix",
|
method: "requestMatrix",
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: false,
|
status: false,
|
||||||
code: this.NETWORK_ERROR,
|
code: NETWORK_ERROR,
|
||||||
message: "Failed to send Matrix notification",
|
message: "Failed to send Matrix notification",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NetworkService;
|
|
||||||
@@ -3,5 +3,5 @@ import type { NotificationMessage } from "@/types/notificationMessage.js";
|
|||||||
|
|
||||||
export interface INotificationProvider {
|
export interface INotificationProvider {
|
||||||
sendMessage: (notification: Notification, message: NotificationMessage) => Promise<boolean>;
|
sendMessage: (notification: Notification, message: NotificationMessage) => Promise<boolean>;
|
||||||
sendTestAlert(notification: Notification): Promise<boolean>;
|
sendTestAlert(notification: Partial<Notification>): Promise<boolean>;
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ export class DiscordProvider implements INotificationProvider {
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTestAlert = async (notification: Notification) => {
|
sendTestAlert = async (notification: Partial<Notification>) => {
|
||||||
if (!notification.address) {
|
if (!notification.address) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class EmailProvider implements INotificationProvider {
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||||
const subject = "Test notification";
|
const subject = "Test notification";
|
||||||
const html = await buildTestEmail(this.emailService);
|
const html = await buildTestEmail(this.emailService);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class MatrixProvider implements INotificationProvider {
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTestAlert = async (notification: Notification) => {
|
sendTestAlert = async (notification: Partial<Notification>) => {
|
||||||
const { homeserverUrl, accessToken, roomId } = notification;
|
const { homeserverUrl, accessToken, roomId } = notification;
|
||||||
if (!homeserverUrl || !accessToken || !roomId) {
|
if (!homeserverUrl || !accessToken || !roomId) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class PagerDutyProvider implements INotificationProvider {
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await got.post("https://events.pagerduty.com/v2/enqueue", {
|
await got.post("https://events.pagerduty.com/v2/enqueue", {
|
||||||
json: {
|
json: {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class SlackProvider implements INotificationProvider {
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||||
if (!notification.address) {
|
if (!notification.address) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
const SERVICE_NAME = "TeamsProvider";
|
||||||
|
import type { Notification } from "@/types/index.js";
|
||||||
|
import { INotificationProvider } from "@/service/index.js";
|
||||||
|
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||||
|
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||||
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
|
import got, { HTTPError } from "got";
|
||||||
|
|
||||||
|
// Types for Adaptive Card elements
|
||||||
|
type TextBlock = {
|
||||||
|
type: "TextBlock";
|
||||||
|
text: string;
|
||||||
|
weight?: "Bolder" | "Normal" | "Lighter";
|
||||||
|
size?: "Small" | "Medium" | "Large" | "ExtraLarge";
|
||||||
|
color?: "Default" | "Dark" | "Light" | "Accent" | "Good" | "Warning" | "Attention";
|
||||||
|
wrap?: boolean;
|
||||||
|
spacing?: "None" | "Small" | "Medium" | "Large" | "ExtraLarge" | "Auto";
|
||||||
|
isSubtle?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ColumnSet = {
|
||||||
|
type: "ColumnSet";
|
||||||
|
separator?: boolean;
|
||||||
|
spacing?: "None" | "Small" | "Medium" | "Large" | "ExtraLarge" | "Auto";
|
||||||
|
columns: unknown[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Fact = {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FactSet = {
|
||||||
|
type: "FactSet";
|
||||||
|
facts: Fact[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Action = {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AdaptiveCard = {
|
||||||
|
type: "AdaptiveCard";
|
||||||
|
$schema: "http://adaptivecards.io/schemas/adaptive-card.json";
|
||||||
|
version: "1.4";
|
||||||
|
body: (TextBlock | ColumnSet | FactSet)[];
|
||||||
|
actions?: Action[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TeamsMessage = {
|
||||||
|
type: "message";
|
||||||
|
attachments: Array<{
|
||||||
|
contentType: "application/vnd.microsoft.card.adaptive";
|
||||||
|
contentUrl: null;
|
||||||
|
content: AdaptiveCard;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TeamsProvider implements INotificationProvider {
|
||||||
|
private logger: ILogger;
|
||||||
|
|
||||||
|
constructor(logger: ILogger) {
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||||
|
if (!notification.address) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = this.wrapAdaptiveCard({
|
||||||
|
type: "AdaptiveCard",
|
||||||
|
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||||
|
version: "1.4",
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: "TextBlock",
|
||||||
|
text: getTestMessage(),
|
||||||
|
weight: "Bolder",
|
||||||
|
size: "Medium",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await got.post(notification.address, {
|
||||||
|
json: payload,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as HTTPError;
|
||||||
|
this.logger.warn({
|
||||||
|
message: "Teams test alert failed",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "sendTestAlert",
|
||||||
|
stack: err?.stack,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||||
|
if (!notification.address) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = this.wrapAdaptiveCard(this.buildAdaptiveCard(message));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await got.post(notification.address, {
|
||||||
|
json: payload,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.info({
|
||||||
|
message: "Teams notification sent via sendMessage",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "sendMessage",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as HTTPError;
|
||||||
|
this.logger.warn({
|
||||||
|
message: "Teams alert failed via sendMessage",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "sendMessage",
|
||||||
|
stack: err?.stack,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap an Adaptive Card in the Teams webhook envelope format
|
||||||
|
*/
|
||||||
|
private wrapAdaptiveCard(card: AdaptiveCard): TeamsMessage {
|
||||||
|
return {
|
||||||
|
type: "message",
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: "application/vnd.microsoft.card.adaptive",
|
||||||
|
contentUrl: null,
|
||||||
|
content: card,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an Adaptive Card from NotificationMessage
|
||||||
|
*/
|
||||||
|
private buildAdaptiveCard(message: NotificationMessage): AdaptiveCard {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
critical: "attention",
|
||||||
|
warning: "warning",
|
||||||
|
success: "good",
|
||||||
|
info: "accent",
|
||||||
|
};
|
||||||
|
const color = (colorMap[message.severity] || "default") as "Default" | "Dark" | "Light" | "Accent" | "Good" | "Warning" | "Attention" | undefined;
|
||||||
|
|
||||||
|
const body: (TextBlock | ColumnSet | FactSet)[] = [];
|
||||||
|
|
||||||
|
// Header with colored status indicator
|
||||||
|
body.push({
|
||||||
|
type: "TextBlock",
|
||||||
|
text: message.content.title,
|
||||||
|
weight: "Bolder",
|
||||||
|
size: "Large",
|
||||||
|
color,
|
||||||
|
wrap: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
body.push({
|
||||||
|
type: "TextBlock",
|
||||||
|
text: message.content.summary,
|
||||||
|
wrap: true,
|
||||||
|
spacing: "Small",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
body.push({
|
||||||
|
type: "ColumnSet",
|
||||||
|
separator: true,
|
||||||
|
spacing: "Medium",
|
||||||
|
columns: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor details as a FactSet
|
||||||
|
body.push({
|
||||||
|
type: "FactSet",
|
||||||
|
facts: [
|
||||||
|
{ title: "Name", value: message.monitor.name },
|
||||||
|
{ title: "Type", value: message.monitor.type },
|
||||||
|
{ title: "Status", value: message.monitor.status },
|
||||||
|
{ title: "URL", value: message.monitor.url },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Threshold breaches
|
||||||
|
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||||
|
body.push({
|
||||||
|
type: "TextBlock",
|
||||||
|
text: "**Threshold Breaches**",
|
||||||
|
weight: "Bolder",
|
||||||
|
spacing: "Medium",
|
||||||
|
wrap: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const breach of message.content.thresholds) {
|
||||||
|
body.push({
|
||||||
|
type: "TextBlock",
|
||||||
|
text: `• **${breach.metric.toUpperCase()}**: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`,
|
||||||
|
wrap: true,
|
||||||
|
spacing: "Small",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional details
|
||||||
|
if (message.content.details && message.content.details.length > 0) {
|
||||||
|
body.push({
|
||||||
|
type: "TextBlock",
|
||||||
|
text: "**Additional Information**",
|
||||||
|
weight: "Bolder",
|
||||||
|
spacing: "Medium",
|
||||||
|
wrap: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const detail of message.content.details) {
|
||||||
|
body.push({
|
||||||
|
type: "TextBlock",
|
||||||
|
text: `• ${detail}`,
|
||||||
|
wrap: true,
|
||||||
|
spacing: "Small",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp footer
|
||||||
|
body.push({
|
||||||
|
type: "TextBlock",
|
||||||
|
text: `Checkmate | ${new Date(message.content.timestamp).toUTCString()}`,
|
||||||
|
size: "Small",
|
||||||
|
isSubtle: true,
|
||||||
|
spacing: "Medium",
|
||||||
|
wrap: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actions (incident link)
|
||||||
|
const actions: Action[] = [];
|
||||||
|
if (message.content.incident) {
|
||||||
|
actions.push({
|
||||||
|
type: "Action.OpenUrl",
|
||||||
|
title: "View Incident",
|
||||||
|
url: `${message.clientHost}/incidents/${message.content.incident.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "AdaptiveCard",
|
||||||
|
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||||
|
version: "1.4",
|
||||||
|
body,
|
||||||
|
...(actions.length > 0 ? { actions } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,7 +98,7 @@ export class WebhookProvider implements INotificationProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTestAlert = async (notification: Notification) => {
|
sendTestAlert = async (notification: Partial<Notification>) => {
|
||||||
if (!notification.address) {
|
if (!notification.address) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import { ILogger } from "@/utils/logger.js";
|
|||||||
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
||||||
|
|
||||||
export interface INotificationsService {
|
export interface INotificationsService {
|
||||||
createNotification: (notificationData: Partial<Notification>) => Promise<Notification>;
|
createNotification: (notificationData: Partial<Notification>, userId: string, teamId: string) => Promise<Notification>;
|
||||||
findById: (id: string, teamId: string) => Promise<Notification>;
|
findById: (id: string, teamId: string) => Promise<Notification>;
|
||||||
findNotificationsByTeamId: (teamId: string) => Promise<Notification[]>;
|
findNotificationsByTeamId: (teamId: string) => Promise<Notification[]>;
|
||||||
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
|
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
|
||||||
deleteById: (id: string, teamId: string) => Promise<Notification>;
|
deleteById: (id: string, teamId: string) => Promise<Notification>;
|
||||||
handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise<boolean>;
|
handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise<boolean>;
|
||||||
|
|
||||||
sendTestNotification: (notification: Notification) => Promise<boolean>;
|
sendTestNotification: (notification: Partial<Notification>) => Promise<boolean>;
|
||||||
testAllNotifications: (notificationIds: string[]) => Promise<boolean>;
|
testAllNotifications: (notificationIds: string[]) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ export class NotificationsService implements INotificationsService {
|
|||||||
private discordProvider: INotificationProvider;
|
private discordProvider: INotificationProvider;
|
||||||
private pagerDutyProvider: INotificationProvider;
|
private pagerDutyProvider: INotificationProvider;
|
||||||
private matrixProvider: INotificationProvider;
|
private matrixProvider: INotificationProvider;
|
||||||
|
private teamsProvider: INotificationProvider;
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
private settingsService: ISettingsService;
|
private settingsService: ISettingsService;
|
||||||
private notificationMessageBuilder: INotificationMessageBuilder;
|
private notificationMessageBuilder: INotificationMessageBuilder;
|
||||||
@@ -45,6 +46,7 @@ export class NotificationsService implements INotificationsService {
|
|||||||
discordProvider: INotificationProvider,
|
discordProvider: INotificationProvider,
|
||||||
pagerDutyProvider: INotificationProvider,
|
pagerDutyProvider: INotificationProvider,
|
||||||
matrixProvider: INotificationProvider,
|
matrixProvider: INotificationProvider,
|
||||||
|
teamsProvider: INotificationProvider,
|
||||||
settingsService: ISettingsService,
|
settingsService: ISettingsService,
|
||||||
logger: ILogger,
|
logger: ILogger,
|
||||||
notificationMessageBuilder: INotificationMessageBuilder
|
notificationMessageBuilder: INotificationMessageBuilder
|
||||||
@@ -57,6 +59,7 @@ export class NotificationsService implements INotificationsService {
|
|||||||
this.discordProvider = discordProvider;
|
this.discordProvider = discordProvider;
|
||||||
this.pagerDutyProvider = pagerDutyProvider;
|
this.pagerDutyProvider = pagerDutyProvider;
|
||||||
this.matrixProvider = matrixProvider;
|
this.matrixProvider = matrixProvider;
|
||||||
|
this.teamsProvider = teamsProvider;
|
||||||
this.settingsService = settingsService;
|
this.settingsService = settingsService;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.notificationMessageBuilder = notificationMessageBuilder;
|
this.notificationMessageBuilder = notificationMessageBuilder;
|
||||||
@@ -92,6 +95,8 @@ export class NotificationsService implements INotificationsService {
|
|||||||
return await this.discordProvider.sendMessage!(notification, notificationMessage);
|
return await this.discordProvider.sendMessage!(notification, notificationMessage);
|
||||||
case "email":
|
case "email":
|
||||||
return await this.emailProvider.sendMessage!(notification, notificationMessage);
|
return await this.emailProvider.sendMessage!(notification, notificationMessage);
|
||||||
|
case "teams":
|
||||||
|
return await this.teamsProvider.sendMessage!(notification, notificationMessage);
|
||||||
default:
|
default:
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `Unknown notification type: ${notification.type}`,
|
message: `Unknown notification type: ${notification.type}`,
|
||||||
@@ -136,7 +141,7 @@ export class NotificationsService implements INotificationsService {
|
|||||||
return await this.sendNotifications(monitor, monitorStatusResponse, decision);
|
return await this.sendNotifications(monitor, monitorStatusResponse, decision);
|
||||||
};
|
};
|
||||||
|
|
||||||
sendTestNotification = async (notification: Notification) => {
|
sendTestNotification = async (notification: Partial<Notification>) => {
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
case "email":
|
case "email":
|
||||||
return await this.emailProvider.sendTestAlert(notification);
|
return await this.emailProvider.sendTestAlert(notification);
|
||||||
@@ -150,6 +155,8 @@ export class NotificationsService implements INotificationsService {
|
|||||||
return await this.matrixProvider.sendTestAlert(notification);
|
return await this.matrixProvider.sendTestAlert(notification);
|
||||||
case "webhook":
|
case "webhook":
|
||||||
return await this.webhookProvider.sendTestAlert(notification);
|
return await this.webhookProvider.sendTestAlert(notification);
|
||||||
|
case "teams":
|
||||||
|
return await this.teamsProvider.sendTestAlert(notification);
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -167,7 +174,9 @@ export class NotificationsService implements INotificationsService {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
createNotification = async (notificationData: Partial<Notification>): Promise<Notification> => {
|
createNotification = async (notificationData: Partial<Notification>, userId: string, teamId: string): Promise<Notification> => {
|
||||||
|
notificationData.userId = userId;
|
||||||
|
notificationData.teamId = teamId;
|
||||||
return await this.notificationsRepository.create(notificationData);
|
return await this.notificationsRepository.create(notificationData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { ISettingsRepository } from "@/repositories/index.js";
|
|||||||
import { Settings } from "@/types/index.js";
|
import { Settings } from "@/types/index.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { ValidatedEnv } from "@/validation/envValidation.js";
|
import { ValidatedEnv } from "@/validation/envValidation.js";
|
||||||
|
import type { StringValue } from "ms";
|
||||||
const SERVICE_NAME = "SettingsService";
|
const SERVICE_NAME = "SettingsService";
|
||||||
|
|
||||||
export type EnvConfig = {
|
export type EnvConfig = {
|
||||||
jwtSecret: string;
|
jwtSecret: string;
|
||||||
jwtTTL: string;
|
jwtTTL: StringValue;
|
||||||
nodeEnv: string;
|
nodeEnv: string;
|
||||||
logLevel: string;
|
logLevel: string;
|
||||||
clientHost: string;
|
clientHost: string;
|
||||||
@@ -21,7 +21,7 @@ export interface ISettingsService {
|
|||||||
getDBSettings(): Promise<Settings>;
|
getDBSettings(): Promise<Settings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsService implements ISettingsService {
|
export class SettingsService implements ISettingsService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private settings: EnvConfig;
|
private settings: EnvConfig;
|
||||||
private settingsRepository: ISettingsRepository;
|
private settingsRepository: ISettingsRepository;
|
||||||
@@ -30,7 +30,7 @@ class SettingsService implements ISettingsService {
|
|||||||
this.settingsRepository = settingsRepository;
|
this.settingsRepository = settingsRepository;
|
||||||
this.settings = {
|
this.settings = {
|
||||||
jwtSecret: env.JWT_SECRET,
|
jwtSecret: env.JWT_SECRET,
|
||||||
jwtTTL: env.TOKEN_TTL,
|
jwtTTL: env.TOKEN_TTL as StringValue,
|
||||||
nodeEnv: env.NODE_ENV,
|
nodeEnv: env.NODE_ENV,
|
||||||
logLevel: env.LOG_LEVEL,
|
logLevel: env.LOG_LEVEL,
|
||||||
clientHost: env.CLIENT_HOST,
|
clientHost: env.CLIENT_HOST,
|
||||||
@@ -74,5 +74,3 @@ class SettingsService implements ISettingsService {
|
|||||||
return settings;
|
return settings;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsService;
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { MonitorType } from "@/types/index.js";
|
import type { MonitorType } from "@/types/index.js";
|
||||||
import type { Response } from "got";
|
import type { Response } from "got";
|
||||||
|
|
||||||
|
export const CHECK_TTL_SENTINEL = 366;
|
||||||
|
|
||||||
export type GotTimings = Response["timings"];
|
export type GotTimings = Response["timings"];
|
||||||
|
|
||||||
export interface CheckMetadata {
|
export interface CheckMetadata {
|
||||||
@@ -98,9 +101,6 @@ export interface Check {
|
|||||||
timings?: GotTimings;
|
timings?: GotTimings;
|
||||||
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[];
|
||||||
@@ -113,7 +113,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -205,4 +204,4 @@ export type NormalizedUptimeCheck<T extends GroupedCheck = GroupedCheck> = T & {
|
|||||||
originalAvgResponseTime: number;
|
originalAvgResponseTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CheckSnapshot = Omit<Check, "metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt">;
|
export type CheckSnapshot = Omit<Check, "metadata" | "updatedAt">;
|
||||||
@@ -3,13 +3,17 @@ export type { CheckSnapshot } from "@/types/check.js";
|
|||||||
import type { GeoContinent } from "@/types/geoCheck.js";
|
import type { GeoContinent } from "@/types/geoCheck.js";
|
||||||
export type { GeoContinent } from "@/types/geoCheck.js";
|
export type { GeoContinent } from "@/types/geoCheck.js";
|
||||||
|
|
||||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "unknown"] as const;
|
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] 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): boolean => GeoCheckSupportedTypes.includes(type);
|
||||||
|
|
||||||
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
|
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
|
||||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||||
|
|
||||||
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
|
export const MonitorMatchMethods = ["equal", "include", "regex"] as const;
|
||||||
|
export type MonitorMatchMethod = (typeof MonitorMatchMethods)[number] | "";
|
||||||
|
|
||||||
export interface Monitor {
|
export interface Monitor {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import type {
|
|||||||
MonitorStatus,
|
MonitorStatus,
|
||||||
} from "@/types/index.js";
|
} from "@/types/index.js";
|
||||||
|
|
||||||
|
import type { QueryResult } from "gamedig";
|
||||||
|
|
||||||
export interface MonitorStatusResponse<
|
export interface MonitorStatusResponse<
|
||||||
T =
|
T =
|
||||||
| HttpStatusPayload
|
| HttpStatusPayload
|
||||||
@@ -22,7 +24,8 @@ export interface MonitorStatusResponse<
|
|||||||
| HardwareStatusPayload
|
| HardwareStatusPayload
|
||||||
| DockerStatusPayload
|
| DockerStatusPayload
|
||||||
| GameStatusPayload
|
| GameStatusPayload
|
||||||
| GrpcStatusPayload,
|
| GrpcStatusPayload
|
||||||
|
| WebSocketStatusPayload,
|
||||||
> {
|
> {
|
||||||
monitorId: string;
|
monitorId: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -49,7 +52,7 @@ export interface PingStatusPayload {
|
|||||||
host: string;
|
host: string;
|
||||||
numeric_host?: string;
|
numeric_host?: string;
|
||||||
alive: boolean;
|
alive: boolean;
|
||||||
time: number;
|
time: number | unknown;
|
||||||
times?: number[];
|
times?: number[];
|
||||||
output?: string;
|
output?: string;
|
||||||
min?: string;
|
min?: string;
|
||||||
@@ -100,7 +103,7 @@ export interface PortStatusPayload {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GameStatusPayload = Record<string, unknown>;
|
export type GameStatusPayload = QueryResult;
|
||||||
|
|
||||||
export interface GrpcStatusPayload {
|
export interface GrpcStatusPayload {
|
||||||
grpcStatusCode: number;
|
grpcStatusCode: number;
|
||||||
@@ -109,6 +112,10 @@ export interface GrpcStatusPayload {
|
|||||||
servingStatus: string;
|
servingStatus: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WebSocketStatusPayload {
|
||||||
|
connected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MonitorPayloadMap {
|
export interface MonitorPayloadMap {
|
||||||
ping: PingStatusPayload;
|
ping: PingStatusPayload;
|
||||||
http: HttpStatusPayload;
|
http: HttpStatusPayload;
|
||||||
@@ -118,7 +125,8 @@ export interface MonitorPayloadMap {
|
|||||||
port: PortStatusPayload;
|
port: PortStatusPayload;
|
||||||
game: GameStatusPayload;
|
game: GameStatusPayload;
|
||||||
grpc: GrpcStatusPayload;
|
grpc: GrpcStatusPayload;
|
||||||
default: unknown;
|
websocket: WebSocketStatusPayload;
|
||||||
|
unknown: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StatusChangeResult = {
|
export type StatusChangeResult = {
|
||||||
@@ -134,3 +142,5 @@ export type StatusChangeResult = {
|
|||||||
temp: boolean;
|
temp: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MonitorStatusResponseOverrides<T> = Partial<Omit<MonitorStatusResponse<T>, "monitorId" | "teamId" | "type">>;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as const;
|
export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as const;
|
||||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
|
|||||||
Vendored
+24
@@ -0,0 +1,24 @@
|
|||||||
|
declare module "ssl-checker" {
|
||||||
|
import { Agent } from "https";
|
||||||
|
|
||||||
|
export interface SSLDetails {
|
||||||
|
daysRemaining: number;
|
||||||
|
valid: boolean;
|
||||||
|
validFrom: string;
|
||||||
|
validTo: string;
|
||||||
|
validFor: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SSLOptions {
|
||||||
|
method?: "GET" | "HEAD";
|
||||||
|
port?: number;
|
||||||
|
agent?: Agent;
|
||||||
|
rejectUnauthorized?: boolean;
|
||||||
|
validateSubjectAltName?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sslChecker(hostname: string, options?: SSLOptions): Promise<SSLDetails>;
|
||||||
|
|
||||||
|
// Change 'export default' to 'export =' to allow it to be callable
|
||||||
|
export = sslChecker;
|
||||||
|
}
|
||||||
@@ -11,34 +11,32 @@ export const getChecksParamValidation = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getChecksQueryValidation = z.object({
|
export const getChecksQueryValidation = z.object({
|
||||||
type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc"]).optional(),
|
type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"]).optional(),
|
||||||
sortOrder: z.enum(["asc", "desc"]).optional(),
|
sortOrder: z.enum(["asc", "desc"]),
|
||||||
limit: z.coerce.number().optional(),
|
limit: z.coerce.number().optional(),
|
||||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
|
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
|
||||||
filter: z.enum(["all", "up", "down", "resolve"]).optional(),
|
filter: z.enum(["all", "up", "down", "resolve"]).optional(),
|
||||||
ack: booleanCoercion.optional(),
|
ack: booleanCoercion.optional(),
|
||||||
page: z.coerce.number().optional(),
|
page: z.coerce.number(),
|
||||||
rowsPerPage: z.coerce.number().optional(),
|
rowsPerPage: z.coerce.number(),
|
||||||
status: booleanCoercion.optional(),
|
status: booleanCoercion.optional(),
|
||||||
continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(),
|
continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getTeamChecksQueryValidation = z.object({
|
export const getTeamChecksQueryValidation = z.object({
|
||||||
sortOrder: z.enum(["asc", "desc"]).optional(),
|
sortOrder: z.enum(["asc", "desc"]),
|
||||||
limit: z.coerce.number().optional(),
|
limit: z.coerce.number().optional(),
|
||||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
|
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
|
||||||
filter: z.enum(["all", "up", "down", "resolve"]).optional(),
|
filter: z.enum(["all", "up", "down", "resolve"]).optional(),
|
||||||
ack: booleanCoercion.optional(),
|
ack: booleanCoercion.optional(),
|
||||||
page: z.coerce.number().optional(),
|
page: z.coerce.number(),
|
||||||
rowsPerPage: z.coerce.number().optional(),
|
rowsPerPage: z.coerce.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteChecksParamValidation = z.object({
|
export const deleteChecksParamValidation = z.object({
|
||||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteChecksByTeamIdParamValidation = z.object({});
|
export const getChecksSummaryByTeamIdQueryValidation = z.object({
|
||||||
|
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
|
||||||
export const updateChecksTTLBodyValidation = z.object({
|
|
||||||
ttl: z.number().min(1, "TTL is required"),
|
|
||||||
});
|
});
|
||||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const envSchema = z.object({
|
const envSchema = z.object({
|
||||||
// Server Configuration
|
// Server Configuration
|
||||||
|
PORT: z.string().default("52345"),
|
||||||
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
||||||
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("debug"),
|
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("debug"),
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { booleanCoercion } from "./shared.js";
|
||||||
|
|
||||||
|
//****************************************
|
||||||
|
// Incident Validations
|
||||||
|
//****************************************
|
||||||
|
|
||||||
|
export const getIncidentsByTeamQueryValidation = z.object({
|
||||||
|
sortOrder: z.enum(["asc", "desc"]),
|
||||||
|
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
|
||||||
|
page: z.coerce.number().int().min(0),
|
||||||
|
rowsPerPage: z.coerce.number().int().min(1),
|
||||||
|
status: booleanCoercion.optional(),
|
||||||
|
monitorId: z.string().optional(),
|
||||||
|
resolutionType: z.enum(["manual", "automatic"]).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getIncidentSummaryQueryValidation = z.object({
|
||||||
|
limit: z.coerce.number().int().min(1).optional(),
|
||||||
|
});
|
||||||
@@ -20,3 +20,4 @@ export * from "./settingsValidation.js";
|
|||||||
export * from "./statusPageValidation.js";
|
export * from "./statusPageValidation.js";
|
||||||
export * from "./notificationValidation.js";
|
export * from "./notificationValidation.js";
|
||||||
export * from "./userValidation.js";
|
export * from "./userValidation.js";
|
||||||
|
export * from "./incidentValidation.js";
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { booleanCoercion } from "./shared.js";
|
import { booleanCoercion } from "./shared.js";
|
||||||
|
|
||||||
|
const dateToString = z.coerce.date().transform((d) => d.toISOString());
|
||||||
|
|
||||||
//****************************************
|
//****************************************
|
||||||
// Maintenance Window Validations
|
// Maintenance Window Validations
|
||||||
//****************************************
|
//****************************************
|
||||||
@@ -11,10 +13,10 @@ export const createMaintenanceWindowBodyValidation = z.object({
|
|||||||
active: z.boolean().optional(),
|
active: z.boolean().optional(),
|
||||||
duration: z.number().min(1, "Duration is required"),
|
duration: z.number().min(1, "Duration is required"),
|
||||||
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]),
|
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]),
|
||||||
start: z.coerce.date(),
|
start: dateToString,
|
||||||
end: z.coerce.date(),
|
end: dateToString,
|
||||||
repeat: z.number().min(0, "Repeat must be a non-negative number"),
|
repeat: z.number().min(0, "Repeat must be a non-negative number"),
|
||||||
expiry: z.coerce.date().optional(),
|
expiry: dateToString.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getMaintenanceWindowByIdParamValidation = z.object({
|
export const getMaintenanceWindowByIdParamValidation = z.object({
|
||||||
@@ -45,10 +47,10 @@ export const editMaintenanceByIdWindowBodyValidation = z.object({
|
|||||||
active: z.boolean().optional(),
|
active: z.boolean().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
repeat: z.number().optional(),
|
repeat: z.number().optional(),
|
||||||
start: z.coerce.date().optional(),
|
start: dateToString.optional(),
|
||||||
end: z.coerce.date().optional(),
|
end: dateToString.optional(),
|
||||||
expiry: z.coerce.date().optional(),
|
expiry: dateToString.optional(),
|
||||||
monitors: z.array(z.unknown()).optional(),
|
monitors: z.array(z.string()).optional(),
|
||||||
duration: z.number().optional(),
|
duration: z.number().optional(),
|
||||||
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(),
|
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(),
|
||||||
});
|
});
|
||||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user