Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c4780e75b | ||
|
|
c24b41ecd9 | ||
|
|
94cf4f17d1 | ||
|
|
0a6d876f4f | ||
|
|
833e2b63ad | ||
|
|
bc2bab87ba | ||
|
|
ee5f7427e7 | ||
|
|
d5968b841e | ||
|
|
26bac6d35a | ||
|
|
3e010f1c0d | ||
|
|
1a504a3cd9 | ||
|
|
120c5cd472 | ||
|
|
cdf17bf600 | ||
|
|
7144c20c5b | ||
|
|
7456609e35 | ||
|
|
05c293d689 | ||
|
|
80f3168837 | ||
|
|
332300dde4 | ||
|
|
d4a8023a76 | ||
|
|
c7e449d18c | ||
|
|
0495bef469 | ||
|
|
6fb7ad63f6 | ||
|
|
8322adbb26 | ||
|
|
4470c87d42 | ||
|
|
95bda35ec6 | ||
|
|
928ab7551e | ||
|
|
465cea21bc | ||
|
|
404bd5d06b | ||
|
|
05814854df | ||
|
|
a4a3249b5b | ||
|
|
1e6cf858c4 | ||
|
|
533a8911cd | ||
|
|
2fb7516ca1 | ||
|
|
6d2008e8d3 | ||
|
|
7332b15242 | ||
|
|
d618a861e3 | ||
|
|
f69752ccb4 | ||
|
|
210881a375 | ||
|
|
d8ec8d7775 | ||
|
|
bfd3eee94d | ||
|
|
bfbb4c390a | ||
|
|
1285a641e8 | ||
|
|
78566c0226 | ||
|
|
f5ba387261 | ||
|
|
48b415a67c | ||
|
|
b53f0e56ea | ||
|
|
07f828a01a | ||
|
|
b09203664d | ||
|
|
5a12105c3d | ||
|
|
22f79144d8 | ||
|
|
f749ed46a6 | ||
|
|
5c23ebe315 | ||
|
|
115fcc67f6 | ||
|
|
622446c308 | ||
|
|
294a149fd1 | ||
|
|
d3d00353a1 | ||
|
|
894b6a7a7d | ||
|
|
3a9f9f0ce5 | ||
|
|
45618a2ab0 | ||
|
|
d80e232a10 | ||
|
|
1706012e54 | ||
|
|
db9d5cf5e4 | ||
|
|
52a6b4c9fa | ||
|
|
9646e7dbdc | ||
|
|
d6ceedc2dd | ||
|
|
501664509b | ||
|
|
2ef184c58a | ||
|
|
22f917034d | ||
|
|
81f623b632 | ||
|
|
d3605b0149 | ||
|
|
48e71d4345 | ||
|
|
411270d256 | ||
|
|
6404a06999 | ||
|
|
a2bc2d8442 | ||
|
|
e9cb302d6e | ||
|
|
fa8764fde2 | ||
|
|
84ca5db0fa | ||
|
|
8ec285ed41 | ||
|
|
d070a33d7f | ||
|
|
aa61e0f321 | ||
|
|
88a39fe502 | ||
|
|
a6b92d3c51 | ||
|
|
1af609a569 | ||
|
|
61cc0c2bef | ||
|
|
e4a228e61e | ||
|
|
842fa92cec | ||
|
|
9466090332 | ||
|
|
01f2aa294e | ||
|
|
365b6cc60a | ||
|
|
24d571a8db | ||
|
|
d41c5e9ebf | ||
|
|
3dd0e0a998 | ||
|
|
bcff3e15a8 | ||
|
|
1ad39fcac6 | ||
|
|
943ef92053 | ||
|
|
f9a4659cb2 | ||
|
|
0fc338db2f | ||
|
|
5578fdf17f | ||
|
|
2a1645baf2 | ||
|
|
8483a7bc55 | ||
|
|
27dd565bc6 | ||
|
|
c5f0316990 | ||
|
|
a40a76fb3c | ||
|
|
4d7de2d3a1 | ||
|
|
9c17fbe79c | ||
|
|
8eca81c8be | ||
|
|
eb2d8b8d3b | ||
|
|
4827f8faec | ||
|
|
9e06758997 |
No files matched your search
Generated
+3
-22
@@ -83,7 +83,6 @@
|
|||||||
"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",
|
||||||
@@ -354,7 +353,6 @@
|
|||||||
"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",
|
||||||
@@ -392,7 +390,6 @@
|
|||||||
"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",
|
||||||
@@ -1162,7 +1159,6 @@
|
|||||||
"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",
|
||||||
@@ -1267,7 +1263,6 @@
|
|||||||
"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",
|
||||||
@@ -2308,7 +2303,6 @@
|
|||||||
"version": "24.5.2",
|
"version": "24.5.2",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.12.0"
|
"undici-types": "~7.12.0"
|
||||||
}
|
}
|
||||||
@@ -2324,7 +2318,6 @@
|
|||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.27",
|
"version": "18.3.27",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -2446,7 +2439,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2771,7 +2763,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -3203,8 +3194,7 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -3586,7 +3576,6 @@
|
|||||||
"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",
|
||||||
@@ -4331,7 +4320,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.6"
|
"@babel/runtime": "^7.27.6"
|
||||||
},
|
},
|
||||||
@@ -5115,7 +5103,6 @@
|
|||||||
"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",
|
||||||
@@ -5763,7 +5750,6 @@
|
|||||||
"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"
|
||||||
@@ -5775,7 +5761,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -5975,8 +5960,7 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -6122,11 +6106,10 @@
|
|||||||
"license": "Unlicense"
|
"license": "Unlicense"
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.59.0",
|
"version": "4.57.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
"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"
|
||||||
},
|
},
|
||||||
@@ -6915,7 +6898,6 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -7051,7 +7033,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"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",
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export const HeatmapResponseTimeTooltip = ({
|
|||||||
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
|
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography>
|
<Typography>
|
||||||
{t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms
|
{t("common.labels.responseTime")}:{" "}
|
||||||
|
{check?.originalResponseTime?.toFixed() ?? "N/A"} ms
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography textTransform={"capitalize"}>
|
<Typography textTransform={"capitalize"}>
|
||||||
Status:{" "}
|
Status:{" "}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { settingsSchema } from "@/Validation/settings";
|
import { settingsSchema } from "@/Validation/settings";
|
||||||
import type { Settings } from "@/Types/Settings";
|
import type { Settings } from "@/Types/Settings";
|
||||||
import type { SettingsFormData } from "@/Validation/settings";
|
import type { SettingsFormInput } from "@/Validation/settings";
|
||||||
|
|
||||||
interface UseSettingsFormOptions {
|
interface UseSettingsFormOptions {
|
||||||
data?: Settings | null;
|
data?: Settings | null;
|
||||||
}
|
}
|
||||||
export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => {
|
export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const defaults: SettingsFormData = {
|
const defaults: SettingsFormInput = {
|
||||||
systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false,
|
systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false,
|
||||||
systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false,
|
systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false,
|
||||||
systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true,
|
systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true,
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ export const useStatusPageForm = ({
|
|||||||
companyName: data?.companyName || "",
|
companyName: data?.companyName || "",
|
||||||
url: data?.url || generateDefaultUrl(),
|
url: data?.url || generateDefaultUrl(),
|
||||||
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
type: data?.type || ["uptime"],
|
||||||
color: data?.color || "#4169E1",
|
color: data?.color || "#4169E1",
|
||||||
monitors: data?.monitors || [],
|
monitors: data?.monitors || [],
|
||||||
isPublished: data?.isPublished ?? false,
|
isPublished: data?.isPublished ?? false,
|
||||||
showCharts: data?.showCharts ?? true,
|
showCharts: data?.showCharts ?? true,
|
||||||
showUptimePercentage: data?.showUptimePercentage ?? true,
|
showUptimePercentage: data?.showUptimePercentage ?? true,
|
||||||
showAdminLoginLink: data?.showAdminLoginLink ?? false,
|
showAdminLoginLink: data?.showAdminLoginLink ?? false,
|
||||||
|
showInfrastructure: data?.showInfrastructure ?? false,
|
||||||
customCSS: data?.customCSS || "",
|
customCSS: data?.customCSS || "",
|
||||||
logo: transformLogo(data?.logo),
|
logo: transformLogo(data?.logo),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -302,49 +302,49 @@ const CreateMonitorPage = () => {
|
|||||||
>
|
>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="http"
|
value="http"
|
||||||
label={t("pages.createMonitor.form.type.optionHttp")}
|
label={t("pages.common.monitors.monitorTypes.optionHttp")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionHttpDescription"
|
"pages.createMonitor.form.type.optionHttpDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="ping"
|
value="ping"
|
||||||
label={t("pages.createMonitor.form.type.optionPing")}
|
label={t("pages.common.monitors.monitorTypes.optionPing")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionPingDescription"
|
"pages.createMonitor.form.type.optionPingDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="docker"
|
value="docker"
|
||||||
label={t("pages.createMonitor.form.type.optionDocker")}
|
label={t("pages.common.monitors.monitorTypes.optionDocker")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionDockerDescription"
|
"pages.createMonitor.form.type.optionDockerDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="port"
|
value="port"
|
||||||
label={t("pages.createMonitor.form.type.optionPort")}
|
label={t("pages.common.monitors.monitorTypes.optionPort")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionPortDescription"
|
"pages.createMonitor.form.type.optionPortDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="game"
|
value="game"
|
||||||
label={t("pages.createMonitor.form.type.optionGame")}
|
label={t("pages.common.monitors.monitorTypes.optionGame")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionGameDescription"
|
"pages.createMonitor.form.type.optionGameDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="grpc"
|
value="grpc"
|
||||||
label={t("pages.createMonitor.form.type.optionGrpc")}
|
label={t("pages.common.monitors.monitorTypes.optionGrpc")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionGrpcDescription"
|
"pages.createMonitor.form.type.optionGrpcDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="websocket"
|
value="websocket"
|
||||||
label={t("pages.createMonitor.form.type.optionWebSocket")}
|
label={t("pages.common.monitors.monitorTypes.optionWebSocket")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionWebSocketDescription"
|
"pages.createMonitor.form.type.optionWebSocketDescription"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
|
|||||||
import { useToast } from "@/Hooks/UseToast";
|
import { useToast } from "@/Hooks/UseToast";
|
||||||
import { useSettingsForm } from "@/Hooks/useSettingsForm";
|
import { useSettingsForm } from "@/Hooks/useSettingsForm";
|
||||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||||
import type { SettingsFormData } from "@/Validation/settings";
|
import type { SettingsFormData, SettingsFormInput } from "@/Validation/settings";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs";
|
import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs";
|
||||||
@@ -89,7 +89,7 @@ export const SettingsPage = () => {
|
|||||||
// Initialize form with schema and defaults
|
// Initialize form with schema and defaults
|
||||||
const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings });
|
const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings });
|
||||||
|
|
||||||
const form = useForm<SettingsFormData>({
|
const form = useForm<SettingsFormInput, unknown, SettingsFormData>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: defaults,
|
defaultValues: defaults,
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
@@ -256,28 +256,7 @@ export const SettingsPage = () => {
|
|||||||
delete (dataToSend as any).systemEmailPassword;
|
delete (dataToSend as any).systemEmailPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert undefined to empty string for backend unsetting
|
const result = await patch("/settings", dataToSend as SettingsFormData);
|
||||||
const processedData = Object.entries(dataToSend).reduce((acc, [key, value]) => {
|
|
||||||
const typedKey = key as keyof SettingsFormData;
|
|
||||||
if (value === undefined) {
|
|
||||||
(acc as any)[typedKey] = "";
|
|
||||||
} else if (typeof value === "object" && value !== null) {
|
|
||||||
// Handle nested objects like globalThresholds
|
|
||||||
// Convert 0 to "" for unsetting thresholds
|
|
||||||
(acc as any)[typedKey] = Object.entries(value).reduce(
|
|
||||||
(nested, [nestedKey, nestedValue]) => ({
|
|
||||||
...nested,
|
|
||||||
[nestedKey]: nestedValue === undefined ? "" : nestedValue,
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
(acc as any)[typedKey] = value;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {} as Partial<SettingsFormData>);
|
|
||||||
|
|
||||||
const result = await patch("/settings", processedData as SettingsFormData);
|
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
// Update API key state from response
|
// Update API key state from response
|
||||||
|
|||||||
@@ -21,19 +21,28 @@ import {
|
|||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
|
import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
|
||||||
import type { StatusPageFormData } from "@/Validation/statusPage";
|
import type { StatusPageFormData } from "@/Validation/statusPage";
|
||||||
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
|
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
|
||||||
import type { Monitor } from "@/Types/Monitor";
|
import type { Monitor } from "@/Types/Monitor";
|
||||||
import type { StatusPageResponse } from "@/Types/StatusPage";
|
import type { MonitorDisplayType, StatusPageResponse } from "@/Types/StatusPage";
|
||||||
|
import { getMonitorTypeLabel } from "@/Types/StatusPage";
|
||||||
import timezones from "@/Utils/timezones.json";
|
import timezones from "@/Utils/timezones.json";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls";
|
import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls";
|
||||||
|
|
||||||
|
const monitorsUrl = (() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
["http", "ping", "port", "docker", "game", "grpc", "websocket", "hardware"].forEach(
|
||||||
|
(type) => params.append("type", type)
|
||||||
|
);
|
||||||
|
return `/monitors/team?${params.toString()}`;
|
||||||
|
})();
|
||||||
|
|
||||||
interface TimezoneOption {
|
interface TimezoneOption {
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -49,11 +58,11 @@ const CreateStatusPage = () => {
|
|||||||
|
|
||||||
// Fetch existing status page data when configuring
|
// Fetch existing status page data when configuring
|
||||||
const { data: statusPageData, isLoading: isLoadingStatusPage } =
|
const { data: statusPageData, isLoading: isLoadingStatusPage } =
|
||||||
useGet<StatusPageResponse>(isCreate ? null : `/status-page/${url}?type=uptime`);
|
useGet<StatusPageResponse>(
|
||||||
|
isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure`
|
||||||
|
);
|
||||||
|
|
||||||
const { data: monitorsResponse } = useGet<Monitor[]>(
|
const { data: monitorsResponse } = useGet<Monitor[]>(monitorsUrl);
|
||||||
"/monitors/team?type=http&type=ping&type=port&type=docker"
|
|
||||||
);
|
|
||||||
const monitors = monitorsResponse ?? [];
|
const monitors = monitorsResponse ?? [];
|
||||||
|
|
||||||
const { post, loading: isSubmittingPost } = usePost();
|
const { post, loading: isSubmittingPost } = usePost();
|
||||||
@@ -81,6 +90,24 @@ const CreateStatusPage = () => {
|
|||||||
reset(defaults);
|
reset(defaults);
|
||||||
}, [defaults, reset]);
|
}, [defaults, reset]);
|
||||||
|
|
||||||
|
const watchedMonitorIds: string[] = form.watch("monitors") ?? [];
|
||||||
|
const computedTypes: MonitorDisplayType[] = useMemo(() => {
|
||||||
|
const selectedMonitors = (watchedMonitorIds ?? [])
|
||||||
|
.map((id) => monitors.find((m) => m.id === id))
|
||||||
|
.filter((m): m is Monitor => m !== undefined);
|
||||||
|
|
||||||
|
const typesSet = new Set<MonitorDisplayType>();
|
||||||
|
selectedMonitors.forEach((m) => {
|
||||||
|
typesSet.add(m.type === "hardware" ? "infrastructure" : "uptime");
|
||||||
|
});
|
||||||
|
|
||||||
|
return typesSet.size ? Array.from(typesSet) : ["uptime"];
|
||||||
|
}, [watchedMonitorIds, monitors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setValue("type", computedTypes);
|
||||||
|
}, [computedTypes]);
|
||||||
|
|
||||||
const onError = (errors: any) => {
|
const onError = (errors: any) => {
|
||||||
logger.debug("Status page validation errors", errors);
|
logger.debug("Status page validation errors", errors);
|
||||||
};
|
};
|
||||||
@@ -103,7 +130,6 @@ const CreateStatusPage = () => {
|
|||||||
|
|
||||||
const onSubmit = async (data: StatusPageFormData) => {
|
const onSubmit = async (data: StatusPageFormData) => {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("type", "uptime");
|
|
||||||
fd.append("isPublished", String(data.isPublished));
|
fd.append("isPublished", String(data.isPublished));
|
||||||
if (data.companyName) fd.append("companyName", data.companyName);
|
if (data.companyName) fd.append("companyName", data.companyName);
|
||||||
if (data.url) fd.append("url", data.url);
|
if (data.url) fd.append("url", data.url);
|
||||||
@@ -112,11 +138,16 @@ const CreateStatusPage = () => {
|
|||||||
fd.append("showCharts", String(data.showCharts));
|
fd.append("showCharts", String(data.showCharts));
|
||||||
fd.append("showUptimePercentage", String(data.showUptimePercentage));
|
fd.append("showUptimePercentage", String(data.showUptimePercentage));
|
||||||
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
|
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
|
||||||
|
fd.append("showInfrastructure", String(data.showInfrastructure));
|
||||||
|
|
||||||
data.monitors.forEach((monitorId) => {
|
data.monitors.forEach((monitorId) => {
|
||||||
fd.append("monitors[]", monitorId);
|
fd.append("monitors[]", monitorId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
data.type.forEach((type) => {
|
||||||
|
fd.append("type[]", type);
|
||||||
|
});
|
||||||
|
|
||||||
// Handle logo upload
|
// Handle logo upload
|
||||||
if (data.logo === null) {
|
if (data.logo === null) {
|
||||||
// Signal to remove the logo
|
// Signal to remove the logo
|
||||||
@@ -303,7 +334,9 @@ const CreateStatusPage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GripVertical size={20} />
|
<GripVertical size={20} />
|
||||||
<Typography flexGrow={1}>{monitor.name}</Typography>
|
<Typography
|
||||||
|
flexGrow={1}
|
||||||
|
>{`${monitor.name} (${getMonitorTypeLabel(monitor.type, t)})`}</Typography>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -425,6 +458,23 @@ const CreateStatusPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
name="showInfrastructure"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t(
|
||||||
|
"pages.statusPages.form.features.option.showInfrastructure.label"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
{/* <Controller
|
{/* <Controller
|
||||||
name="showUptimePercentage"
|
name="showUptimePercentage"
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Gauge } from "@/Components/design-elements";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
import type { Monitor } from "@/Types/Monitor";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||||
|
|
||||||
|
const GAUGE_RADIUS = 60;
|
||||||
|
const GAUGE_STROKE_WIDTH = 12;
|
||||||
|
const PERCENTAGE_MULTIPLIER = 100;
|
||||||
|
|
||||||
|
interface StatusPageMonitor extends Monitor {
|
||||||
|
checks?: Monitor["recentChecks"];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricDetail {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricConfig {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
hasData: boolean;
|
||||||
|
progress: number;
|
||||||
|
details: MetricDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetricDetailRow = ({ label, value }: MetricDetail) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">{value}</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MetricItemProps {
|
||||||
|
label: string;
|
||||||
|
progress: number;
|
||||||
|
details?: MetricDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetricItem = ({ label, progress, details }: MetricItemProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
size={isSmall ? 12 : 4}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
gap: theme.spacing(SPACING.LG),
|
||||||
|
padding: theme.spacing(LAYOUT.XS),
|
||||||
|
borderRight: isSmall ? "none" : `1px solid ${theme.palette.divider}`,
|
||||||
|
borderBottom: isSmall ? `1px solid ${theme.palette.divider}` : "none",
|
||||||
|
"&:last-child": {
|
||||||
|
borderRight: "none",
|
||||||
|
borderBottom: "none",
|
||||||
|
paddingBottom: theme.spacing(SPACING.LG),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Gauge
|
||||||
|
progress={progress}
|
||||||
|
radius={GAUGE_RADIUS}
|
||||||
|
strokeWidth={GAUGE_STROKE_WIDTH}
|
||||||
|
/>
|
||||||
|
<Typography variant="body2">{label}</Typography>
|
||||||
|
</Box>
|
||||||
|
{details && details.length > 0 && (
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
paddingX={theme.spacing(LAYOUT.LG)}
|
||||||
|
>
|
||||||
|
{details.map((detail) => (
|
||||||
|
<MetricDetailRow
|
||||||
|
key={detail.label}
|
||||||
|
label={detail.label}
|
||||||
|
value={detail.value}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type LatestCheck = NonNullable<Monitor["recentChecks"]>[number];
|
||||||
|
|
||||||
|
const buildCpuMetric = (
|
||||||
|
check: LatestCheck,
|
||||||
|
t: (key: string) => string
|
||||||
|
): MetricConfig | null => {
|
||||||
|
if (!check.cpu || typeof check.cpu.usage_percent !== "number") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const usagePercent = (check.cpu.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER;
|
||||||
|
return {
|
||||||
|
key: "cpu",
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.cpu"),
|
||||||
|
hasData: true,
|
||||||
|
progress: usagePercent,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.usage"),
|
||||||
|
value: `${usagePercent.toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildMemoryMetric = (
|
||||||
|
check: LatestCheck,
|
||||||
|
t: (key: string) => string
|
||||||
|
): MetricConfig | null => {
|
||||||
|
if (
|
||||||
|
!check.memory ||
|
||||||
|
typeof check.memory.usage_percent !== "number" ||
|
||||||
|
typeof check.memory.used_bytes !== "number" ||
|
||||||
|
typeof check.memory.total_bytes !== "number"
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: "memory",
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.memory"),
|
||||||
|
hasData: true,
|
||||||
|
progress: (check.memory.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.used"),
|
||||||
|
value: prettyBytes(check.memory.used_bytes ?? 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.total"),
|
||||||
|
value: prettyBytes(check.memory.total_bytes ?? 0),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildDiskMetric = (
|
||||||
|
check: LatestCheck,
|
||||||
|
t: (key: string) => string
|
||||||
|
): MetricConfig | null => {
|
||||||
|
if (!check.disk || check.disk.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const disks = check.disk;
|
||||||
|
const avgUsagePercent =
|
||||||
|
(disks.reduce((acc, disk) => acc + (disk?.usage_percent ?? 0), 0) / disks.length) *
|
||||||
|
PERCENTAGE_MULTIPLIER;
|
||||||
|
const totalUsedBytes = disks.reduce((acc, disk) => acc + (disk?.used_bytes ?? 0), 0);
|
||||||
|
const totalTotalBytes = disks.reduce((acc, disk) => acc + (disk?.total_bytes ?? 0), 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: "disk",
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.disk"),
|
||||||
|
hasData: true,
|
||||||
|
progress: avgUsagePercent,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.used"),
|
||||||
|
value: prettyBytes(totalUsedBytes),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.total"),
|
||||||
|
value: prettyBytes(totalTotalBytes),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0];
|
||||||
|
|
||||||
|
if (!latestCheck) {
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{t("pages.statusPages.monitorsList.noData")}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics: MetricConfig[] = [
|
||||||
|
buildCpuMetric(latestCheck, t),
|
||||||
|
buildMemoryMetric(latestCheck, t),
|
||||||
|
buildDiskMetric(latestCheck, t),
|
||||||
|
].filter((m): m is MetricConfig => m !== null);
|
||||||
|
|
||||||
|
if (metrics.length === 0) {
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{t("pages.statusPages.monitorsList.noData")}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
alignItems="center"
|
||||||
|
padding={theme.spacing(LAYOUT.XS)}
|
||||||
|
>
|
||||||
|
{metrics.map(({ key, label, progress, details }) => (
|
||||||
|
<MetricItem
|
||||||
|
key={key}
|
||||||
|
label={label}
|
||||||
|
progress={progress}
|
||||||
|
details={details}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,13 +5,16 @@ import Typography from "@mui/material/Typography";
|
|||||||
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common";
|
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common";
|
||||||
import { StatusLabel, BaseBox } from "@/Components/design-elements";
|
import { StatusLabel, BaseBox } from "@/Components/design-elements";
|
||||||
import { SwitchComponent } from "@/Components/inputs";
|
import { SwitchComponent } from "@/Components/inputs";
|
||||||
|
import { InfrastructureMetrics } from "@/Pages/StatusPage/Status/Components/InfrastructureMetrics";
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme, type Theme } from "@mui/material/styles";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { Monitor } from "@/Types/Monitor";
|
import type { Monitor } from "@/Types/Monitor";
|
||||||
import type { StatusPage } from "@/Types/StatusPage";
|
import type { StatusPage } from "@/Types/StatusPage";
|
||||||
|
import { getMonitorTypeLabel } from "@/Types/StatusPage";
|
||||||
import type { RootState } from "@/Types/state";
|
import type { RootState } from "@/Types/state";
|
||||||
|
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||||
|
|
||||||
interface StatusPageMonitor extends Monitor {
|
interface StatusPageMonitor extends Monitor {
|
||||||
checks?: Monitor["recentChecks"];
|
checks?: Monitor["recentChecks"];
|
||||||
@@ -22,29 +25,124 @@ interface MonitorsListProps {
|
|||||||
monitors: StatusPageMonitor[];
|
monitors: StatusPageMonitor[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => {
|
||||||
|
const bg =
|
||||||
|
monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light;
|
||||||
|
return {
|
||||||
|
backgroundColor: bg,
|
||||||
|
color: theme.palette.background.paper,
|
||||||
|
padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const MonitorHeader = ({
|
||||||
|
monitor,
|
||||||
|
showURL,
|
||||||
|
}: {
|
||||||
|
monitor: StatusPageMonitor;
|
||||||
|
showURL: boolean;
|
||||||
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const showURL = useSelector((state: RootState) => state.ui?.showURL);
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
gap={theme.spacing(LAYOUT.XS)}
|
||||||
|
mb={theme.spacing(LAYOUT.XS)}
|
||||||
|
>
|
||||||
|
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
gap={theme.spacing(SPACING.LG)}
|
||||||
|
mb={theme.spacing(SPACING.SM)}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
{monitor.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={getMonitorBadgeStyles(monitor.type ?? "", theme)}
|
||||||
|
>
|
||||||
|
{getMonitorTypeLabel(monitor.type, t)}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
{showURL && monitor.url && (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
{monitor.url}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<StatusLabel status={monitor.status} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MonitorContent = ({
|
||||||
|
monitor,
|
||||||
|
statusPage,
|
||||||
|
chartType,
|
||||||
|
}: {
|
||||||
|
monitor: StatusPageMonitor;
|
||||||
|
statusPage: StatusPage;
|
||||||
|
chartType: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (monitor.type === "hardware") {
|
||||||
|
if (statusPage.showInfrastructure === false) return null;
|
||||||
|
return <InfrastructureMetrics monitor={monitor} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusPage.showCharts === false) return null;
|
||||||
|
|
||||||
|
const checks = monitor.checks?.slice().reverse() ?? [];
|
||||||
|
return (
|
||||||
|
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1, mb: theme.spacing(SPACING.LG) }}>
|
||||||
|
{chartType === "histogram" ? (
|
||||||
|
<HistogramResponseTime
|
||||||
|
height={{ xs: 50, md: 100 }}
|
||||||
|
gap={{ xs: theme.spacing(SPACING.SM), md: theme.spacing(LAYOUT.SM) }}
|
||||||
|
checks={checks}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HeatmapResponseTime checks={checks} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const showURL = useSelector((state: RootState) => state.ui?.showURL);
|
||||||
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
|
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={theme.spacing(8)}>
|
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||||
{statusPage.showCharts !== false && (
|
{statusPage.showCharts && (
|
||||||
<Stack
|
<Stack
|
||||||
direction={"row"}
|
direction="row"
|
||||||
alignItems={"center"}
|
alignItems="center"
|
||||||
|
gap={theme.spacing(LAYOUT.SM)}
|
||||||
>
|
>
|
||||||
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
|
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
|
||||||
<SwitchComponent
|
<SwitchComponent
|
||||||
dualOption
|
dualOption
|
||||||
value={chartType}
|
value={chartType}
|
||||||
checked={chartType === "histogram"}
|
checked={chartType === "histogram"}
|
||||||
onChange={(e) => {
|
onChange={(e) => setChartType(e.target.checked ? "histogram" : "heatmap")}
|
||||||
setChartType(e.target.checked ? "histogram" : "heatmap");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Typography>
|
<Typography>
|
||||||
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
|
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
|
||||||
@@ -52,59 +150,22 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{monitors?.map((monitor) => {
|
{monitors.map((monitor) => (
|
||||||
return (
|
<BaseBox
|
||||||
<BaseBox
|
key={monitor.id}
|
||||||
key={monitor.id}
|
padding={theme.spacing(LAYOUT.MD)}
|
||||||
padding={theme.spacing(4)}
|
>
|
||||||
>
|
<MonitorHeader
|
||||||
<Stack
|
monitor={monitor}
|
||||||
direction="row"
|
showURL={showURL}
|
||||||
alignItems="center"
|
/>
|
||||||
justifyContent="space-between"
|
<MonitorContent
|
||||||
gap={theme.spacing(4)}
|
monitor={monitor}
|
||||||
mb={statusPage.showCharts !== false ? theme.spacing(4) : 0}
|
statusPage={statusPage}
|
||||||
>
|
chartType={chartType}
|
||||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
/>
|
||||||
<Typography
|
</BaseBox>
|
||||||
variant="h6"
|
))}
|
||||||
sx={{
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{monitor.name}
|
|
||||||
</Typography>
|
|
||||||
{showURL && (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
>
|
|
||||||
{monitor.url}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<StatusLabel status={monitor.status} />
|
|
||||||
</Stack>
|
|
||||||
{statusPage.showCharts !== false && (
|
|
||||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
|
||||||
{chartType === "histogram" ? (
|
|
||||||
<HistogramResponseTime
|
|
||||||
height={{ xs: 50, md: 100 }}
|
|
||||||
gap={{ xs: theme.spacing(0.5), md: theme.spacing(5) }}
|
|
||||||
checks={monitor?.checks?.slice().reverse() ?? []}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<HeatmapResponseTime
|
|
||||||
checks={monitor?.checks?.slice().reverse() ?? []}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</BaseBox>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -23,16 +23,22 @@ const StatusPageView = () => {
|
|||||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
const isPublic = location.pathname.startsWith("/status/public");
|
const isPublic = location.pathname.startsWith("/status/public");
|
||||||
|
|
||||||
const apiUrl = url ? `/status-page/${url}?type=uptime` : null;
|
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
|
||||||
|
|
||||||
const { data, isLoading, error } = useGet<StatusPageResponse>(apiUrl);
|
const { data, isLoading, error } = useGet<StatusPageResponse>(
|
||||||
|
apiUrl,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
refreshInterval: 10000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const statusPage = data?.statusPage;
|
const statusPage = data?.statusPage;
|
||||||
const monitors = data?.monitors ?? [];
|
const monitors = data?.monitors ?? [];
|
||||||
|
|
||||||
if (!statusPage) return null;
|
if (!statusPage) return null;
|
||||||
|
|
||||||
if (monitors?.length === 0) {
|
if (monitors.length === 0) {
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const StatusPagesTable = ({
|
|||||||
{
|
{
|
||||||
id: "type",
|
id: "type",
|
||||||
content: t("common.table.headers.type"),
|
content: t("common.table.headers.type"),
|
||||||
render: (row) => row.type,
|
render: (row) => row.type.join(", "),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "status",
|
id: "status",
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
import type { Monitor } from "@/Types/Monitor";
|
import type { Monitor, MonitorType } from "@/Types/Monitor";
|
||||||
|
export type MonitorDisplayType = "uptime" | "infrastructure";
|
||||||
|
|
||||||
|
export const MONITOR_TYPE_KEYS: Partial<Record<MonitorType, string>> = {
|
||||||
|
http: "pages.common.monitors.monitorTypes.optionHttp",
|
||||||
|
ping: "pages.common.monitors.monitorTypes.optionPing",
|
||||||
|
docker: "pages.common.monitors.monitorTypes.optionDocker",
|
||||||
|
port: "pages.common.monitors.monitorTypes.optionPort",
|
||||||
|
game: "pages.common.monitors.monitorTypes.optionGame",
|
||||||
|
grpc: "pages.common.monitors.monitorTypes.optionGrpc",
|
||||||
|
websocket: "pages.common.monitors.monitorTypes.optionWebSocket",
|
||||||
|
hardware: "pages.common.monitors.monitorTypes.optionHardware",
|
||||||
|
pagespeed: "pages.common.monitors.monitorTypes.optionPagespeed",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMonitorTypeLabel = (
|
||||||
|
type: MonitorType,
|
||||||
|
t: (key: string) => string
|
||||||
|
): string => {
|
||||||
|
const key = MONITOR_TYPE_KEYS[type];
|
||||||
|
return key ? t(key) : type;
|
||||||
|
};
|
||||||
|
|
||||||
export interface StatusPage {
|
export interface StatusPage {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
type: string;
|
type: MonitorDisplayType[];
|
||||||
companyName: string;
|
companyName: string;
|
||||||
url: string;
|
url: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
@@ -20,6 +41,7 @@ export interface StatusPage {
|
|||||||
showCharts: boolean;
|
showCharts: boolean;
|
||||||
showUptimePercentage: boolean;
|
showUptimePercentage: boolean;
|
||||||
showAdminLoginLink: boolean;
|
showAdminLoginLink: boolean;
|
||||||
|
showInfrastructure: boolean;
|
||||||
customCSS: string;
|
customCSS: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const settingsSchema = z.object({
|
|||||||
systemEmailRejectUnauthorized: z.boolean(),
|
systemEmailRejectUnauthorized: z.boolean(),
|
||||||
systemEmailConnectionHost: z
|
systemEmailConnectionHost: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailSecure: z.boolean().optional(),
|
systemEmailSecure: z.boolean().optional(),
|
||||||
systemEmailPool: z.boolean().optional(),
|
systemEmailPool: z.boolean().optional(),
|
||||||
@@ -19,31 +19,30 @@ export const settingsSchema = z.object({
|
|||||||
.max(CHECK_TTL_SENTINEL, `Maximum ${CHECK_TTL_SENTINEL}`),
|
.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() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailHost: z
|
systemEmailHost: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^[a-zA-Z0-9.-]*$/, "Invalid hostname or IP address")
|
.regex(/^[a-zA-Z0-9.-]*$/, "Invalid hostname or IP address")
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailPort: z.number().int().min(1).max(65535).optional(),
|
systemEmailPort: z.number().int().min(1).max(65535).optional(),
|
||||||
systemEmailAddress: z
|
systemEmailAddress: z
|
||||||
.string()
|
|
||||||
.email("Please enter a valid email address")
|
.email("Please enter a valid email address")
|
||||||
.or(z.literal(""))
|
.or(z.literal(""))
|
||||||
.transform((val) => (val === "" ? undefined : val.toLowerCase().trim()))
|
.transform((val) => (val === "" ? null : val.toLowerCase().trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailUser: z
|
systemEmailUser: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailPassword: z
|
systemEmailPassword: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailTLSServername: z
|
systemEmailTLSServername: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
globalThresholds: z.object({
|
globalThresholds: z.object({
|
||||||
cpu: z.number().int().min(1).max(100),
|
cpu: z.number().int().min(1).max(100),
|
||||||
@@ -53,4 +52,5 @@ export const settingsSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type SettingsFormInput = z.input<typeof settingsSchema>;
|
||||||
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
||||||
@@ -14,12 +14,16 @@ export const statusPageSchema = z.object({
|
|||||||
"URL can only contain lowercase letters, numbers, and hyphens"
|
"URL can only contain lowercase letters, numbers, and hyphens"
|
||||||
),
|
),
|
||||||
timezone: z.string().optional(),
|
timezone: z.string().optional(),
|
||||||
|
type: z
|
||||||
|
.array(z.enum(["uptime", "infrastructure"]))
|
||||||
|
.min(1, "At least one type is required"),
|
||||||
color: z.string().min(1, "Color is required"),
|
color: z.string().min(1, "Color is required"),
|
||||||
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
|
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
|
||||||
isPublished: z.boolean(),
|
isPublished: z.boolean(),
|
||||||
showCharts: z.boolean(),
|
showCharts: z.boolean(),
|
||||||
showUptimePercentage: z.boolean(),
|
showUptimePercentage: z.boolean(),
|
||||||
showAdminLoginLink: z.boolean(),
|
showAdminLoginLink: z.boolean(),
|
||||||
|
showInfrastructure: z.boolean(),
|
||||||
customCSS: z.string().optional(),
|
customCSS: z.string().optional(),
|
||||||
logo: z
|
logo: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -288,7 +288,13 @@ const logoImageValidation = joi
|
|||||||
.optional(); // Make entire object optional
|
.optional(); // Make entire object optional
|
||||||
|
|
||||||
const statusPageValidation = joi.object({
|
const statusPageValidation = joi.object({
|
||||||
type: joi.string().valid("uptime").required(),
|
type: joi
|
||||||
|
.alternatives()
|
||||||
|
.try(
|
||||||
|
joi.string().valid("uptime", "infrastructure"),
|
||||||
|
joi.array().items(joi.string().valid("uptime", "infrastructure"))
|
||||||
|
)
|
||||||
|
.required(),
|
||||||
isPublished: joi.bool(),
|
isPublished: joi.bool(),
|
||||||
companyName: joi
|
companyName: joi
|
||||||
.string()
|
.string()
|
||||||
@@ -317,6 +323,7 @@ const statusPageValidation = joi.object({
|
|||||||
showUptimePercentage: joi.boolean(),
|
showUptimePercentage: joi.boolean(),
|
||||||
showCharts: joi.boolean(),
|
showCharts: joi.boolean(),
|
||||||
showAdminLoginLink: joi.boolean(),
|
showAdminLoginLink: joi.boolean(),
|
||||||
|
showInfrastructure: joi.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const settingsValidation = joi.object({
|
const settingsValidation = joi.object({
|
||||||
|
|||||||
@@ -401,12 +401,23 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"down": "down",
|
"down": "down",
|
||||||
"breached": "breached",
|
"breached": "threshold exceeded",
|
||||||
"initializing": "initializing",
|
"initializing": "initializing",
|
||||||
"maintenance": "maintenance",
|
"maintenance": "maintenance",
|
||||||
"paused": "paused",
|
"paused": "paused",
|
||||||
"total": "total",
|
"total": "total",
|
||||||
"up": "up"
|
"up": "up"
|
||||||
|
},
|
||||||
|
"monitorTypes": {
|
||||||
|
"optionDocker": "Docker",
|
||||||
|
"optionGame": "Game",
|
||||||
|
"optionHttp": "HTTP(S)",
|
||||||
|
"optionPing": "Ping",
|
||||||
|
"optionPort": "Port",
|
||||||
|
"optionPagespeed": "PageSpeed",
|
||||||
|
"optionHardware": "Infrastructure",
|
||||||
|
"optionGrpc": "gRPC",
|
||||||
|
"optionWebSocket": "WebSocket"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -534,19 +545,12 @@
|
|||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"description": "Select the type of check to perform",
|
"description": "Select the type of check to perform",
|
||||||
"optionDocker": "Docker",
|
|
||||||
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
||||||
"optionGame": "Game",
|
|
||||||
"optionGameDescription": "Monitor if a specific game server is online.",
|
"optionGameDescription": "Monitor if a specific game server is online.",
|
||||||
"optionGrpc": "gRPC",
|
|
||||||
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
||||||
"optionWebSocket": "WebSocket",
|
|
||||||
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
||||||
"optionHttp": "HTTP(S)",
|
|
||||||
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
||||||
"optionPing": "Ping",
|
|
||||||
"optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
|
"optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
|
||||||
"optionPort": "Port",
|
|
||||||
"optionPortDescription": "Monitor if a specific port on a server is open.",
|
"optionPortDescription": "Monitor if a specific port on a server is open.",
|
||||||
"title": "Type"
|
"title": "Type"
|
||||||
},
|
},
|
||||||
@@ -1178,7 +1182,22 @@
|
|||||||
},
|
},
|
||||||
"monitorsList": {
|
"monitorsList": {
|
||||||
"chartTypeHeatmap": "Heatmap",
|
"chartTypeHeatmap": "Heatmap",
|
||||||
"chartTypeHistogram": "Histogram"
|
"chartTypeHistogram": "Histogram",
|
||||||
|
"noData": "No data available",
|
||||||
|
"infrastructure": {
|
||||||
|
"title": "Infrastructure",
|
||||||
|
"cpu": "CPU",
|
||||||
|
"memory": "RAM",
|
||||||
|
"memoryText": "Memory",
|
||||||
|
"disk": "Disk",
|
||||||
|
"usage": "Usage",
|
||||||
|
"used": "Used",
|
||||||
|
"total": "Total"
|
||||||
|
},
|
||||||
|
"uptime": {
|
||||||
|
"title": "Uptime",
|
||||||
|
"responseTime": "Response time"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"statusBar": {
|
"statusBar": {
|
||||||
"allDown": "All systems are down",
|
"allDown": "All systems are down",
|
||||||
@@ -1270,6 +1289,9 @@
|
|||||||
},
|
},
|
||||||
"showAdminLoginLink": {
|
"showAdminLoginLink": {
|
||||||
"label": "Show admin login link"
|
"label": "Show admin login link"
|
||||||
|
},
|
||||||
|
"showInfrastructure": {
|
||||||
|
"label": "Show infrastructure metrics"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,11 +32,14 @@ export default [
|
|||||||
languageOptions: {
|
languageOptions: {
|
||||||
parser: tseslint.parser,
|
parser: tseslint.parser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
projectService: true,
|
projectService: {
|
||||||
|
allowDefaultProject: ["jest.config.ts"],
|
||||||
|
},
|
||||||
tsconfigRootDir: import.meta.dirname,
|
tsconfigRootDir: import.meta.dirname,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], // Ignore unused variables that start with '_'
|
||||||
"@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types
|
"@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types
|
||||||
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
|
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
|
||||||
},
|
},
|
||||||
|
|||||||
+174
-137
@@ -510,42 +510,24 @@
|
|||||||
"summary": "Get monitors by team",
|
"summary": "Get monitors by team",
|
||||||
"description": "Get monitors filtered by team with optional parameters",
|
"description": "Get monitors filtered by team with optional parameters",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
|
||||||
"name": "status",
|
|
||||||
"in": "query",
|
|
||||||
"description": "Filter by monitor status (up/down)",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "type",
|
"name": "type",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"description": "Filter by monitor type",
|
"description": "Filter by monitor type",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"oneOf": [
|
||||||
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"]
|
{
|
||||||
}
|
"type": "string",
|
||||||
},
|
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
|
||||||
{
|
},
|
||||||
"name": "page",
|
{
|
||||||
"in": "query",
|
"type": "array",
|
||||||
"description": "Page number for pagination",
|
"items": {
|
||||||
"schema": {
|
"type": "string",
|
||||||
"type": "integer",
|
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
|
||||||
"minimum": 1,
|
}
|
||||||
"default": 1
|
}
|
||||||
}
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "rowsPerPage",
|
|
||||||
"in": "query",
|
|
||||||
"description": "Number of monitors per page",
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 1,
|
|
||||||
"maximum": 100,
|
|
||||||
"default": 10
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -555,15 +537,6 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "field",
|
|
||||||
"in": "query",
|
|
||||||
"description": "Field to filter on",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["name", "url", "description"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -756,7 +729,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"put": {
|
"patch": {
|
||||||
"tags": ["monitors"],
|
"tags": ["monitors"],
|
||||||
"summary": "Update monitor",
|
"summary": "Update monitor",
|
||||||
"description": "Update an existing monitor",
|
"description": "Update an existing monitor",
|
||||||
@@ -3568,22 +3541,19 @@
|
|||||||
},
|
},
|
||||||
"CreateMonitorRequest": {
|
"CreateMonitorRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["name", "description", "type", "url", "interval"],
|
"required": ["name", "type", "url"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
|
||||||
"maxLength": 100,
|
|
||||||
"example": "My Website Monitor"
|
"example": "My Website Monitor"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 500,
|
|
||||||
"example": "Monitors the main website homepage"
|
"example": "Monitors the main website homepage"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"],
|
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"],
|
||||||
"example": "http"
|
"example": "http"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
@@ -3593,14 +3563,34 @@
|
|||||||
},
|
},
|
||||||
"interval": {
|
"interval": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 30,
|
|
||||||
"maximum": 3600,
|
|
||||||
"example": 300,
|
"example": 300,
|
||||||
"description": "Check interval in seconds"
|
"description": "Check interval in seconds"
|
||||||
},
|
},
|
||||||
|
"statusWindowSize": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 20,
|
||||||
|
"default": 5
|
||||||
|
},
|
||||||
|
"statusWindowThreshold": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
"isActive": {
|
"isActive": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"ignoreTlsErrors": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true
|
"default": false
|
||||||
|
},
|
||||||
|
"useAdvancedMatching": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -3609,51 +3599,62 @@
|
|||||||
},
|
},
|
||||||
"description": "Array of notification IDs to associate with this monitor"
|
"description": "Array of notification IDs to associate with this monitor"
|
||||||
},
|
},
|
||||||
"httpOptions": {
|
"secret": {
|
||||||
"type": "object",
|
"type": "string"
|
||||||
"properties": {
|
|
||||||
"method": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["GET", "POST", "PUT", "DELETE", "HEAD"],
|
|
||||||
"default": "GET"
|
|
||||||
},
|
|
||||||
"headers": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"body": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Request body for POST/PUT requests"
|
|
||||||
},
|
|
||||||
"timeout": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 1000,
|
|
||||||
"maximum": 30000,
|
|
||||||
"default": 5000,
|
|
||||||
"description": "Request timeout in milliseconds"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"assertions": {
|
"jsonPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expectedValue": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"matchMethod": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["equal", "include", "regex", ""]
|
||||||
|
},
|
||||||
|
"cpuAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"memoryAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"diskAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tempAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gameId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"grpcServiceName": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"selectedDisks": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "string"
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["status-code", "response-time", "body-contains", "header-contains"]
|
|
||||||
},
|
|
||||||
"comparison": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"]
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"maxLength": 50
|
||||||
|
},
|
||||||
|
"geoCheckEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"geoCheckLocations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geoCheckInterval": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 300000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3661,21 +3662,32 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"]
|
||||||
"maxLength": 100
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"statusWindowSize": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 20,
|
||||||
|
"default": 5
|
||||||
|
},
|
||||||
|
"statusWindowThreshold": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100,
|
||||||
|
"default": 60
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string"
|
||||||
"maxLength": 500
|
|
||||||
},
|
},
|
||||||
"interval": {
|
"interval": {
|
||||||
"type": "integer",
|
"type": "integer"
|
||||||
"minimum": 30,
|
|
||||||
"maximum": 3600
|
|
||||||
},
|
|
||||||
"isActive": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -3683,47 +3695,72 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"httpOptions": {
|
"secret": {
|
||||||
"type": "object",
|
"type": "string"
|
||||||
"properties": {
|
|
||||||
"method": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["GET", "POST", "PUT", "DELETE", "HEAD"]
|
|
||||||
},
|
|
||||||
"headers": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"body": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"timeout": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 1000,
|
|
||||||
"maximum": 30000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"assertions": {
|
"ignoreTlsErrors": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"useAdvancedMatching": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"jsonPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expectedValue": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"matchMethod": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["equal", "include", "regex", ""]
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535
|
||||||
|
},
|
||||||
|
"cpuAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"memoryAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"diskAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tempAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gameId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"grpcServiceName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"selectedDisks": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "string"
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["status-code", "response-time", "body-contains", "header-contains"]
|
|
||||||
},
|
|
||||||
"comparison": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"]
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"maxLength": 50
|
||||||
|
},
|
||||||
|
"geoCheckEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"geoCheckLocations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geoCheckInterval": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 300000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
"test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand",
|
"test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand",
|
||||||
"dev": "nodemon --exec tsx src/index.js",
|
"dev": "nodemon --exec tsx src/index.js",
|
||||||
"start": "node --watch ./dist/index.js",
|
"start": "node --watch ./dist/index.js",
|
||||||
"build": "tsc && tsc-alias",
|
"build": "tsc && tsc-alias && cp -r src/templates dist/templates",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint-fix": "eslint --fix .",
|
"lint-fix": "eslint --fix .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ import cors from "cors";
|
|||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import compression from "compression";
|
import compression from "compression";
|
||||||
import cookieParser from "cookie-parser";
|
import cookieParser from "cookie-parser";
|
||||||
import swaggerUi from "swagger-ui-express";
|
import swaggerUi, { type JsonObject } from "swagger-ui-express";
|
||||||
import { handleErrors } from "@/middleware/handleErrors.js";
|
import { handleErrors } from "@/middleware/handleErrors.js";
|
||||||
import { generalApiLimiter } from "@/middleware/rateLimiter.js";
|
import { generalApiLimiter } from "@/middleware/rateLimiter.js";
|
||||||
import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js";
|
import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js";
|
||||||
@@ -24,7 +24,7 @@ export const createApp = ({
|
|||||||
controllers: InitializedControllers;
|
controllers: InitializedControllers;
|
||||||
envSettings: EnvConfig;
|
envSettings: EnvConfig;
|
||||||
frontendPath: string;
|
frontendPath: string;
|
||||||
openApiSpec: any;
|
openApiSpec: JsonObject;
|
||||||
}) => {
|
}) => {
|
||||||
const allowedOrigin = envSettings.clientHost;
|
const allowedOrigin = envSettings.clientHost;
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export interface InitializedControllers {
|
|||||||
export const initializeControllers = (services: InitializedServices): InitializedControllers => {
|
export const initializeControllers = (services: InitializedServices): InitializedControllers => {
|
||||||
return {
|
return {
|
||||||
authController: new AuthController(services.userService),
|
authController: new AuthController(services.userService),
|
||||||
monitorController: new MonitorController(services.monitorService),
|
monitorController: new MonitorController(services.monitorService, services.notificationsService),
|
||||||
settingsController: new SettingsController(services.settingsService, services.emailService),
|
settingsController: new SettingsController(services.settingsService, services.emailService),
|
||||||
checkController: new CheckController(services.checkService),
|
checkController: new CheckController(services.checkService),
|
||||||
geoCheckController: new GeoCheckController(services.geoChecksService),
|
geoCheckController: new GeoCheckController(services.geoChecksService),
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import type { Application } from "express";
|
||||||
import { createVerifyJWT } from "../middleware/verifyJWT.js";
|
import { createVerifyJWT } from "../middleware/verifyJWT.js";
|
||||||
import { createVerifyStatusPageAccess } from "../middleware/verifyStatusPageAccess.js";
|
import { createVerifyStatusPageAccess } from "../middleware/verifyStatusPageAccess.js";
|
||||||
import { authApiLimiter } from "../middleware/rateLimiter.js";
|
import { authApiLimiter } from "../middleware/rateLimiter.js";
|
||||||
|
import type { InitializedControllers } from "./controllers.js";
|
||||||
|
import type { InitializedServices } from "./services.js";
|
||||||
|
|
||||||
import AuthRoutes from "../routes/authRoute.js";
|
import AuthRoutes from "../routes/authRoute.js";
|
||||||
import InviteRoutes from "../routes/inviteRoute.js";
|
import InviteRoutes from "../routes/inviteRoute.js";
|
||||||
@@ -17,7 +20,7 @@ import NotificationRoutes from "../routes/notificationRoute.js";
|
|||||||
|
|
||||||
import IncidentRoutes from "../routes/incidentRoute.js";
|
import IncidentRoutes from "../routes/incidentRoute.js";
|
||||||
|
|
||||||
export const setupRoutes = (app: any, controllers: Record<string, any>, services: Record<string, any>) => {
|
export const setupRoutes = (app: Application, controllers: InitializedControllers, services: InitializedServices) => {
|
||||||
const verifyJWT = createVerifyJWT(services.settingsService);
|
const verifyJWT = createVerifyJWT(services.settingsService);
|
||||||
const authRoutes = new AuthRoutes(controllers.authController, verifyJWT);
|
const authRoutes = new AuthRoutes(controllers.authController, verifyJWT);
|
||||||
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
|
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
|
||||||
|
|||||||
@@ -23,7 +23,24 @@ import { IUserService } from "@/service/index.js";
|
|||||||
|
|
||||||
const SERVICE_NAME = "authController";
|
const SERVICE_NAME = "authController";
|
||||||
|
|
||||||
class AuthController {
|
export interface IAuthController {
|
||||||
|
registerUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
createUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
loginUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
editUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
checkSuperadminExists(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
requestRecovery(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
validateRecovery(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
resetPassword(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
deleteUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
deleteUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
getAllUsers(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
getUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
editUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
editUserPasswordById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthController implements IAuthController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private userService: IUserService;
|
private userService: IUserService;
|
||||||
|
|||||||
@@ -12,7 +12,16 @@ import { requireTeamId } from "@/controllers/controllerUtils.js";
|
|||||||
|
|
||||||
const SERVICE_NAME = "checkController";
|
const SERVICE_NAME = "checkController";
|
||||||
|
|
||||||
class CheckController {
|
export interface ICheckController {
|
||||||
|
serviceName: string;
|
||||||
|
getChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getChecksByTeam: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getChecksSummaryByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteChecks: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckController implements ICheckController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private checkService: ICheckService;
|
private checkService: ICheckService;
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { IDiagnosticService } from "@/service/index.js";
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
const SERVICE_NAME = "diagnosticController";
|
const SERVICE_NAME = "diagnosticController";
|
||||||
class DiagnosticController {
|
|
||||||
|
export interface IDiagnosticController {
|
||||||
|
getSystemStats: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiagnosticController implements IDiagnosticController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private diagnosticService: IDiagnosticService;
|
private diagnosticService: IDiagnosticService;
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
|
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
|
||||||
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
||||||
|
import { requireTeamId } from "./controllerUtils.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "geoCheckController";
|
const SERVICE_NAME = "geoCheckController";
|
||||||
|
|
||||||
class GeoCheckController {
|
export interface IGeoCheckController {
|
||||||
|
getGeoChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
class GeoCheckController implements IGeoCheckController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private geoChecksService: IGeoChecksService;
|
private geoChecksService: IGeoChecksService;
|
||||||
@@ -18,13 +23,29 @@ class GeoCheckController {
|
|||||||
|
|
||||||
getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
getGeoChecksByMonitor = 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);
|
||||||
|
|
||||||
|
if (!validatedQuery.continent || validatedQuery.continent.length === 0) {
|
||||||
|
throw new AppError({
|
||||||
|
message: "At least one continent must be specified",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getGeoChecksByMonitor",
|
||||||
|
details: { monitorId: validatedParams.monitorId },
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const result = await this.geoChecksService.getGeoChecksByMonitor({
|
const result = await this.geoChecksService.getGeoChecksByMonitor({
|
||||||
monitorId: req?.params?.monitorId as string,
|
monitorId: validatedParams.monitorId,
|
||||||
query: req?.query,
|
teamId: teamId,
|
||||||
teamId: req?.user?.teamId as string,
|
sortOrder: validatedQuery.sortOrder,
|
||||||
|
dateRange: validatedQuery.dateRange,
|
||||||
|
page: validatedQuery.page,
|
||||||
|
rowsPerPage: validatedQuery.rowsPerPage,
|
||||||
|
continent: validatedQuery.continent,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import { IIncidentService } from "@/service/index.js";
|
|||||||
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
|
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "IncidentController";
|
const SERVICE_NAME = "IncidentController";
|
||||||
class IncidentController {
|
|
||||||
|
export interface IIncidentController {
|
||||||
|
getIncidentsByTeam: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getIncidentSummary: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getIncidentById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
resolveIncidentManually: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
class IncidentController implements IIncidentController {
|
||||||
private incidentService: IIncidentService;
|
private incidentService: IIncidentService;
|
||||||
constructor(incidentService: IIncidentService) {
|
constructor(incidentService: IIncidentService) {
|
||||||
this.incidentService = incidentService;
|
this.incidentService = incidentService;
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers
|
|||||||
import { IInviteService } from "@/service/index.js";
|
import { IInviteService } from "@/service/index.js";
|
||||||
const SERVICE_NAME = "inviteController";
|
const SERVICE_NAME = "inviteController";
|
||||||
|
|
||||||
class InviteController {
|
export interface IInviteController {
|
||||||
|
getInviteToken: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
sendInviteEmail: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
verifyInviteToken: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class InviteController implements IInviteController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private inviteService: IInviteService;
|
private inviteService: IInviteService;
|
||||||
constructor(inviteService: IInviteService) {
|
constructor(inviteService: IInviteService) {
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { Request, Response, NextFunction } from "express";
|
|||||||
|
|
||||||
const SERVICE_NAME = "LogController";
|
const SERVICE_NAME = "LogController";
|
||||||
|
|
||||||
|
export interface ILogController {
|
||||||
|
getLogs: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
class LogController {
|
class LogController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
|
|||||||
@@ -11,7 +11,15 @@ import {
|
|||||||
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
||||||
import { IMaintenanceWindowService } from "@/service/index.js";
|
import { IMaintenanceWindowService } from "@/service/index.js";
|
||||||
|
|
||||||
class MaintenanceWindowController {
|
export interface IMaintenanceWindowController {
|
||||||
|
createMaintenanceWindows: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMaintenanceWindowById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMaintenanceWindowsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMaintenanceWindowsByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
editMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
class MaintenanceWindowController implements IMaintenanceWindowController {
|
||||||
private maintenanceWindowService: IMaintenanceWindowService;
|
private maintenanceWindowService: IMaintenanceWindowService;
|
||||||
constructor(maintenanceWindowService: IMaintenanceWindowService) {
|
constructor(maintenanceWindowService: IMaintenanceWindowService) {
|
||||||
this.maintenanceWindowService = maintenanceWindowService;
|
this.maintenanceWindowService = maintenanceWindowService;
|
||||||
@@ -21,7 +29,17 @@ class MaintenanceWindowController {
|
|||||||
try {
|
try {
|
||||||
const validatedBody = 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 });
|
|
||||||
|
const monitorIDs = validatedBody.monitors;
|
||||||
|
const name = validatedBody.name;
|
||||||
|
const active = validatedBody.active ?? true;
|
||||||
|
const duration = validatedBody.duration;
|
||||||
|
const durationUnit = validatedBody.durationUnit;
|
||||||
|
const repeat = validatedBody.repeat;
|
||||||
|
const start = validatedBody.start;
|
||||||
|
const end = validatedBody.end;
|
||||||
|
|
||||||
|
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, monitorIDs, name, active, duration, durationUnit, repeat, start, end });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
|
||||||
import {
|
import {
|
||||||
getMonitorByIdParamValidation,
|
getMonitorByIdParamValidation,
|
||||||
getMonitorByIdQueryValidation,
|
getMonitorByIdQueryValidation,
|
||||||
@@ -16,18 +16,43 @@ import {
|
|||||||
importMonitorsBodyValidation,
|
importMonitorsBodyValidation,
|
||||||
} from "@/validation/monitorValidation.js";
|
} from "@/validation/monitorValidation.js";
|
||||||
import sslChecker from "ssl-checker";
|
import sslChecker from "ssl-checker";
|
||||||
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "./controllerUtils.js";
|
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { IMonitorService } from "@/service/index.js";
|
import { IMonitorService, INotificationsService } from "@/service/index.js";
|
||||||
|
import { GeoContinent } from "@/types/geoCheck.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "monitorController";
|
const SERVICE_NAME = "monitorController";
|
||||||
class MonitorController {
|
|
||||||
|
export interface IMonitorController {
|
||||||
|
getMonitorCertificate: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getUptimeDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getHardwareDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getPageSpeedDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getGeoChecksByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMonitorById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
createMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
importMonitorsFromJSON: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteAllMonitors: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
editMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
pauseMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
addDemoMonitors: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMonitorsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMonitorsWithChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
exportMonitorsToJSON: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getAllGames: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getGroupsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
updateNotifications: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
class MonitorController implements IMonitorController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private monitorService: IMonitorService;
|
private monitorService: IMonitorService;
|
||||||
|
private notificationsService: INotificationsService;
|
||||||
|
|
||||||
constructor(monitorService: IMonitorService) {
|
constructor(monitorService: IMonitorService, notificationsService: INotificationsService) {
|
||||||
this.monitorService = monitorService;
|
this.monitorService = monitorService;
|
||||||
|
this.notificationsService = notificationsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
get serviceName() {
|
get serviceName() {
|
||||||
@@ -60,7 +85,6 @@ class MonitorController {
|
|||||||
|
|
||||||
const monitorId = validatedParams.monitorId;
|
const monitorId = validatedParams.monitorId;
|
||||||
const dateRange = validatedQuery.dateRange;
|
const dateRange = validatedQuery.dateRange;
|
||||||
const normalize = validatedQuery.normalize;
|
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
@@ -68,7 +92,6 @@ class MonitorController {
|
|||||||
teamId,
|
teamId,
|
||||||
monitorId,
|
monitorId,
|
||||||
dateRange,
|
dateRange,
|
||||||
normalize,
|
|
||||||
});
|
});
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -324,7 +347,6 @@ class MonitorController {
|
|||||||
try {
|
try {
|
||||||
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query);
|
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const explain = validatedQuery.explain;
|
|
||||||
const limit = validatedQuery.limit;
|
const limit = validatedQuery.limit;
|
||||||
const page = validatedQuery.page;
|
const page = validatedQuery.page;
|
||||||
const rowsPerPage = validatedQuery.rowsPerPage;
|
const rowsPerPage = validatedQuery.rowsPerPage;
|
||||||
@@ -343,7 +365,6 @@ class MonitorController {
|
|||||||
filter,
|
filter,
|
||||||
field,
|
field,
|
||||||
order,
|
order,
|
||||||
explain,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -396,6 +417,42 @@ class MonitorController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateNotifications = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
updateNotificationsValidation.parse(req.body);
|
||||||
|
|
||||||
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
const { monitorIds, notificationIds, action } = req.body;
|
||||||
|
|
||||||
|
// Verify all requested notification IDs actually belong to this team
|
||||||
|
const teamNotifications = await this.notificationsService.findNotificationsByTeamId(teamId);
|
||||||
|
const validNotificationIds = teamNotifications.map((n) => n.id);
|
||||||
|
|
||||||
|
const invalidIds = notificationIds.filter((id: string) => !validNotificationIds.includes(id));
|
||||||
|
if (invalidIds.length > 0) {
|
||||||
|
throw new AppError({
|
||||||
|
message: `The following notification IDs are invalid or do not belong to your team: ${invalidIds.join(", ")}`,
|
||||||
|
status: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedCount = await this.monitorService.updateNotifications({
|
||||||
|
teamId,
|
||||||
|
monitorIds,
|
||||||
|
notificationIds,
|
||||||
|
action,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`,
|
||||||
|
data: { modifiedCount },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MonitorController;
|
export default MonitorController;
|
||||||
@@ -9,13 +9,22 @@ import {
|
|||||||
testAllNotificationsBodyValidation,
|
testAllNotificationsBodyValidation,
|
||||||
} from "@/validation/notificationValidation.js";
|
} from "@/validation/notificationValidation.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
|
||||||
import { INotificationsService } from "@/service/index.js";
|
import { INotificationsService } from "@/service/index.js";
|
||||||
import { requireTeamId, requireUserId } from "./controllerUtils.js";
|
import { requireTeamId, requireUserId } from "./controllerUtils.js";
|
||||||
|
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "NotificationController";
|
const SERVICE_NAME = "NotificationController";
|
||||||
|
|
||||||
class NotificationController {
|
export interface INotificationController {
|
||||||
|
testNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
createNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getNotificationsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getNotificationById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
editNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
testAllNotifications: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
class NotificationController implements INotificationController {
|
||||||
private notificationsService: INotificationsService;
|
private notificationsService: INotificationsService;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
|
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
|
||||||
@@ -133,8 +142,8 @@ class NotificationController {
|
|||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId);
|
const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId);
|
||||||
|
const notifications = monitor.notifications || [];
|
||||||
|
|
||||||
const notifications = monitor.notifications;
|
|
||||||
if (notifications.length === 0) {
|
if (notifications.length === 0) {
|
||||||
throw new AppError({ message: "No notifications", status: 400 });
|
throw new AppError({ message: "No notifications", status: 400 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
|
import { ISuperSimpleQueue } from "@/service/index.js";
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
const SERVICE_NAME = "JobQueueController";
|
const SERVICE_NAME = "JobQueueController";
|
||||||
|
|
||||||
class JobQueueController {
|
export interface IJobQueueController {
|
||||||
|
getMetrics(req: Request, res: Response, next: NextFunction): Promise<void>;
|
||||||
|
getJobs(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
getAllMetrics(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
flushQueue(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class JobQueueController implements IJobQueueController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private jobQueue: any;
|
private jobQueue: ISuperSimpleQueue;
|
||||||
constructor(jobQueue: any) {
|
constructor(jobQueue: ISuperSimpleQueue) {
|
||||||
this.jobQueue = jobQueue;
|
this.jobQueue = jobQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,18 +61,6 @@ class JobQueueController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
addJob = async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
await this.jobQueue.addJob(Math.random().toString(36).substring(7));
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
msg: "Job added to queue successfully",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
flushQueue = async (req: Request, res: Response, next: NextFunction) => {
|
flushQueue = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const result = await this.jobQueue.flushQueues();
|
const result = await this.jobQueue.flushQueues();
|
||||||
@@ -77,18 +73,5 @@ class JobQueueController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkQueueHealth = async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const stuckQueues = await this.jobQueue.checkQueueHealth();
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
msg: "Queue health checked successfully",
|
|
||||||
data: stuckQueues,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
export default JobQueueController;
|
export default JobQueueController;
|
||||||
@@ -2,14 +2,23 @@ import { Request, Response, NextFunction } from "express";
|
|||||||
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
|
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
|
||||||
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
|
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import { IEmailService, ISettingsService } from "@/service/index.js";
|
||||||
|
import { Settings } from "@/types/settings.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "SettingsController";
|
const SERVICE_NAME = "SettingsController";
|
||||||
|
|
||||||
class SettingsController {
|
export interface ISettingsController {
|
||||||
|
serviceName: string;
|
||||||
|
getAppSettings(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
updateAppSettings(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
sendTestEmail(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsController implements ISettingsController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private settingsService: any;
|
private settingsService: ISettingsService;
|
||||||
private emailService: any;
|
private emailService: IEmailService;
|
||||||
constructor(settingsService: any, emailService: any) {
|
constructor(settingsService: ISettingsService, emailService: IEmailService) {
|
||||||
this.settingsService = settingsService;
|
this.settingsService = settingsService;
|
||||||
this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
}
|
}
|
||||||
@@ -18,11 +27,11 @@ class SettingsController {
|
|||||||
return SettingsController.SERVICE_NAME;
|
return SettingsController.SERVICE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildAppSettings = (dbSettings: any) => {
|
buildAppSettings = (dbSettings: Settings) => {
|
||||||
const sanitizedSettings = { ...dbSettings };
|
const sanitizedSettings: Record<string, unknown> = { ...dbSettings };
|
||||||
delete sanitizedSettings.version;
|
delete sanitizedSettings.version;
|
||||||
delete sanitizedSettings.jwtSecret;
|
delete sanitizedSettings.jwtSecret;
|
||||||
const returnSettings = {
|
const returnSettings: Record<string, unknown | null> = {
|
||||||
pagespeedKeySet: false,
|
pagespeedKeySet: false,
|
||||||
emailPasswordSet: false,
|
emailPasswordSet: false,
|
||||||
settings: null,
|
settings: null,
|
||||||
@@ -57,9 +66,9 @@ class SettingsController {
|
|||||||
|
|
||||||
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
|
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
updateAppSettingsBodyValidation.parse(req.body);
|
const validatedBody = updateAppSettingsBodyValidation.parse(req.body);
|
||||||
|
|
||||||
const updatedSettings = await this.settingsService.updateDbSettings(req.body);
|
const updatedSettings = await this.settingsService.updateDbSettings(validatedBody);
|
||||||
const returnSettings = this.buildAppSettings(updatedSettings);
|
const returnSettings = this.buildAppSettings(updatedSettings);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -95,6 +104,9 @@ class SettingsController {
|
|||||||
const context = { testName: "Monitoring System" };
|
const context = { testName: "Monitoring System" };
|
||||||
|
|
||||||
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
||||||
|
if (!html) {
|
||||||
|
throw new AppError({ message: "Failed to build email template.", status: 500 });
|
||||||
|
}
|
||||||
const messageId = await this.emailService.sendEmail(to, subject, html, {
|
const messageId = await this.emailService.sendEmail(to, subject, html, {
|
||||||
systemEmailHost,
|
systemEmailHost,
|
||||||
systemEmailPort,
|
systemEmailPort,
|
||||||
|
|||||||
@@ -15,7 +15,16 @@ import { NormalizeData } from "@/utils/dataUtils.js";
|
|||||||
|
|
||||||
const SERVICE_NAME = "statusPageController";
|
const SERVICE_NAME = "statusPageController";
|
||||||
|
|
||||||
class StatusPageController {
|
export interface IStatusPageController {
|
||||||
|
readonly serviceName: string;
|
||||||
|
createStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
updateStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
getStatusPageByUrl(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
getStatusPagesByTeamId(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
deleteStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatusPageController implements IStatusPageController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private statusPageService: IStatusPageService;
|
private statusPageService: IStatusPageService;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
@@ -66,7 +75,7 @@ class StatusPageController {
|
|||||||
if (statusPage === null) {
|
if (statusPage === null) {
|
||||||
throw new AppError({ message: "Status page not found", status: 404 });
|
throw new AppError({ message: "Status page not found", status: 404 });
|
||||||
}
|
}
|
||||||
return res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "Status page updated successfully",
|
msg: "Status page updated successfully",
|
||||||
data: statusPage,
|
data: statusPage,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
|
import type { Document, AnyBulkWriteOperation } from "mongodb";
|
||||||
|
|
||||||
const CHECKS_COLLECTION = "checks";
|
const CHECKS_COLLECTION = "checks";
|
||||||
const BACKUP_COLLECTION = "checks_backup";
|
const BACKUP_COLLECTION = "checks_backup";
|
||||||
@@ -85,7 +86,7 @@ const createTimeSeriesCollection = async (backedUp: boolean) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateDocument = (doc: any): boolean => {
|
const validateDocument = (doc: Document): boolean => {
|
||||||
if (!doc.createdAt) {
|
if (!doc.createdAt) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,7 @@ const validateDocument = (doc: any): boolean => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const storeFailedDocument = async (doc: any, reason: string) => {
|
const storeFailedDocument = async (doc: Document, reason: string) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
await db.collection(FAILED_DOCS_COLLECTION).insertOne({
|
await db.collection(FAILED_DOCS_COLLECTION).insertOne({
|
||||||
originalDoc: doc,
|
originalDoc: doc,
|
||||||
@@ -124,8 +125,8 @@ const migrateBackupData = async (backedUp: boolean): Promise<MigrationStats> =>
|
|||||||
stats.totalSource = await source.countDocuments();
|
stats.totalSource = await source.countDocuments();
|
||||||
|
|
||||||
const cursor = source.find().addCursorFlag("noCursorTimeout", true);
|
const cursor = source.find().addCursorFlag("noCursorTimeout", true);
|
||||||
const operations: any[] = [];
|
const operations: AnyBulkWriteOperation<Document>[] = [];
|
||||||
const invalidDocs: any[] = [];
|
const invalidDocs: Document[] = [];
|
||||||
|
|
||||||
while (await cursor.hasNext()) {
|
while (await cursor.hasNext()) {
|
||||||
const doc = await cursor.next();
|
const doc = await cursor.next();
|
||||||
@@ -139,6 +140,7 @@ const migrateBackupData = async (backedUp: boolean): Promise<MigrationStats> =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { _id, monitorId, teamId, type, ...rest } = doc;
|
const { _id, monitorId, teamId, type, ...rest } = doc;
|
||||||
|
void _id;
|
||||||
const metadata = { monitorId, teamId, type };
|
const metadata = { monitorId, teamId, type };
|
||||||
operations.push({ insertOne: { document: { ...rest, metadata } } });
|
operations.push({ insertOne: { document: { ...rest, metadata } } });
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ export async function cleanupDuplicateMonitorStats(): Promise<void> {
|
|||||||
const monitorId = duplicate._id;
|
const monitorId = duplicate._id;
|
||||||
const { ids, updatedAts } = duplicate;
|
const { ids, updatedAts } = duplicate;
|
||||||
|
|
||||||
type DocPair = { id: any; updatedAt: Date };
|
type DocPair = { id: string; updatedAt: Date };
|
||||||
|
|
||||||
// Create array of {id, updatedAt} pairs and sort by updatedAt descending
|
// Create array of {id, updatedAt} pairs and sort by updatedAt descending
|
||||||
const docs: DocPair[] = ids.map((id: any, index: number) => ({
|
const docs: DocPair[] = ids.map((id: string, index: number) => ({
|
||||||
id,
|
id,
|
||||||
updatedAt: updatedAts[index],
|
updatedAt: updatedAts[index],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export async function fixInfrastructureThresholds(): Promise<void> {
|
|||||||
const update: Record<string, number> = {};
|
const update: Record<string, number> = {};
|
||||||
|
|
||||||
for (const field of thresholdFields) {
|
for (const field of thresholdFields) {
|
||||||
if ((monitor as any)[field] === 0 || (monitor as any)[field] === 5) {
|
const value = monitor.get(field);
|
||||||
|
if (value === 0 || value === 5) {
|
||||||
update[field] = 100;
|
update[field] = 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
import { logger } from "@/utils/logger.js";
|
||||||
|
import StatusPageModel from "../models/StatusPage.js";
|
||||||
|
|
||||||
|
export async function migrateStatusPageTypeToArray(): Promise<void> {
|
||||||
|
const SERVICE_NAME = "Migration:MigrateStatusPageTypeToArray";
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info({ service: SERVICE_NAME, message: "Starting migration of StatusPage type field from string to array" });
|
||||||
|
|
||||||
|
const db = mongoose.connection.db;
|
||||||
|
if (!db) {
|
||||||
|
throw new Error("Database connection is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await StatusPageModel.updateMany(
|
||||||
|
{
|
||||||
|
$expr: {
|
||||||
|
$eq: [{ $type: "$type" }, "string"], // only scalar string, not existing array
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
type: ["$type"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.modifiedCount === 0) {
|
||||||
|
logger.info({ service: SERVICE_NAME, message: "No StatusPage documents needed migration" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
message: `Migration complete. Converted ${result.modifiedCount} StatusPage document(s) from string type to array type`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.error({ service: SERVICE_NAME, message: `Error during StatusPage type migration: ${errorMessage}` });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ 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 { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
|
||||||
import MigrationModel from "../models/Migration.js";
|
import MigrationModel from "../models/Migration.js";
|
||||||
|
import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js";
|
||||||
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
type MigrationEntry = {
|
type MigrationEntry = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -14,9 +16,10 @@ const migrations: MigrationEntry[] = [
|
|||||||
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
|
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
|
||||||
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
|
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
|
||||||
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
|
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
|
||||||
|
{ name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray },
|
||||||
];
|
];
|
||||||
|
|
||||||
const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
const runMigrations = async (logger?: ILogger) => {
|
||||||
try {
|
try {
|
||||||
logger?.info({ message: "Running migrations", service: "Migrations" });
|
logger?.info({ message: "Running migrations", service: "Migrations" });
|
||||||
for (const migration of migrations) {
|
for (const migration of migrations) {
|
||||||
@@ -43,7 +46,7 @@ const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
|||||||
logger?.info({ message: "Migrations completed", service: "Migrations" });
|
logger?.info({ message: "Migrations completed", service: "Migrations" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
logger?.error({ message: "Migration failed", service: "Migrations", details: err?.message, stack: err?.stack });
|
logger?.error({ message: "Migration failed", service: "Migrations", details: { error: err?.message }, stack: err?.stack });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Schema, model, type Types } from "mongoose";
|
import { Schema, model, type Types } from "mongoose";
|
||||||
import type { MaintenanceWindow, DurationUnit } from "@/types/maintenanceWindow.js";
|
import type { MaintenanceWindow } from "@/types/maintenanceWindow.js";
|
||||||
|
|
||||||
type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt"> & {
|
type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt"> & {
|
||||||
monitorId: Types.ObjectId;
|
monitorId: Types.ObjectId;
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import { Schema, model, Types, type UpdateQuery } from "mongoose";
|
import { Schema, model, Types } from "mongoose";
|
||||||
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
|
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
|
||||||
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
|
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
|
||||||
|
import type {
|
||||||
|
CheckAudits,
|
||||||
|
CheckCaptureInfo,
|
||||||
|
CheckCpuInfo,
|
||||||
|
CheckDiskInfo,
|
||||||
|
CheckErrorInfo,
|
||||||
|
CheckHostInfo,
|
||||||
|
CheckMemoryInfo,
|
||||||
|
CheckNetworkInterfaceInfo,
|
||||||
|
GotTimings,
|
||||||
|
ILighthouseAudit,
|
||||||
|
} from "@/types/check.js";
|
||||||
|
|
||||||
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
|
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
|
||||||
|
|
||||||
@@ -23,13 +35,167 @@ interface MonitorDocument extends MonitorDocumentBase {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const snapshotTimingPhasesSchema = new Schema<GotTimings["phases"]>(
|
||||||
|
{
|
||||||
|
wait: { type: Number },
|
||||||
|
dns: { type: Number },
|
||||||
|
tcp: { type: Number },
|
||||||
|
tls: { type: Number },
|
||||||
|
request: { type: Number },
|
||||||
|
firstByte: { type: Number },
|
||||||
|
download: { type: Number },
|
||||||
|
total: { type: Number },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotTimingsSchema = new Schema<GotTimings>(
|
||||||
|
{
|
||||||
|
start: { type: Number },
|
||||||
|
socket: { type: Number },
|
||||||
|
lookup: { type: Number },
|
||||||
|
connect: { type: Number },
|
||||||
|
secureConnect: { type: Number },
|
||||||
|
upload: { type: Number },
|
||||||
|
response: { type: Number },
|
||||||
|
end: { type: Number },
|
||||||
|
phases: { type: snapshotTimingPhasesSchema },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotCpuSchema = new Schema<CheckCpuInfo>(
|
||||||
|
{
|
||||||
|
physical_core: { type: Number },
|
||||||
|
logical_core: { type: Number },
|
||||||
|
frequency: { type: Number },
|
||||||
|
current_frequency: { type: Number },
|
||||||
|
temperature: { type: [Number] },
|
||||||
|
free_percent: { type: Number },
|
||||||
|
usage_percent: { type: Number },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotMemorySchema = new Schema<CheckMemoryInfo>(
|
||||||
|
{
|
||||||
|
total_bytes: { type: Number },
|
||||||
|
available_bytes: { type: Number },
|
||||||
|
used_bytes: { type: Number },
|
||||||
|
usage_percent: { type: Number },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotDiskSchema = new Schema<CheckDiskInfo>(
|
||||||
|
{
|
||||||
|
device: { type: String },
|
||||||
|
mountpoint: { type: String },
|
||||||
|
total_bytes: { type: Number },
|
||||||
|
free_bytes: { type: Number },
|
||||||
|
used_bytes: { type: Number },
|
||||||
|
usage_percent: { type: Number },
|
||||||
|
total_inodes: { type: Number },
|
||||||
|
free_inodes: { type: Number },
|
||||||
|
used_inodes: { type: Number },
|
||||||
|
inodes_usage_percent: { type: Number },
|
||||||
|
read_bytes: { type: Number },
|
||||||
|
write_bytes: { type: Number },
|
||||||
|
read_time: { type: Number },
|
||||||
|
write_time: { type: Number },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotHostSchema = new Schema<CheckHostInfo>(
|
||||||
|
{
|
||||||
|
os: { type: String },
|
||||||
|
platform: { type: String },
|
||||||
|
kernel_version: { type: String },
|
||||||
|
pretty_name: { type: String },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotErrorSchema = new Schema<CheckErrorInfo>(
|
||||||
|
{
|
||||||
|
metric: { type: [String] },
|
||||||
|
err: { type: String },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotCaptureSchema = new Schema<CheckCaptureInfo>(
|
||||||
|
{
|
||||||
|
version: { type: String },
|
||||||
|
mode: { type: String },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotNetworkInterfaceSchema = new Schema<CheckNetworkInterfaceInfo>(
|
||||||
|
{
|
||||||
|
name: { type: String },
|
||||||
|
bytes_sent: { type: Number },
|
||||||
|
bytes_recv: { type: Number },
|
||||||
|
packets_sent: { type: Number },
|
||||||
|
packets_recv: { type: Number },
|
||||||
|
err_in: { type: Number },
|
||||||
|
err_out: { type: Number },
|
||||||
|
drop_in: { type: Number },
|
||||||
|
drop_out: { type: Number },
|
||||||
|
fifo_in: { type: Number },
|
||||||
|
fifo_out: { type: Number },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotLighthouseAuditSchema = new Schema<ILighthouseAudit>(
|
||||||
|
{
|
||||||
|
id: { type: String },
|
||||||
|
title: { type: String },
|
||||||
|
score: { type: Number },
|
||||||
|
displayValue: { type: String },
|
||||||
|
numericValue: { type: Number },
|
||||||
|
numericUnit: { type: String },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotAuditsSchema = new Schema<CheckAudits>(
|
||||||
|
{
|
||||||
|
cls: { type: snapshotLighthouseAuditSchema },
|
||||||
|
si: { type: snapshotLighthouseAuditSchema },
|
||||||
|
fcp: { type: snapshotLighthouseAuditSchema },
|
||||||
|
lcp: { type: snapshotLighthouseAuditSchema },
|
||||||
|
tbt: { type: snapshotLighthouseAuditSchema },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
const checkSnapshotSchema = new Schema<CheckSnapshotDocument>(
|
const checkSnapshotSchema = new Schema<CheckSnapshotDocument>(
|
||||||
{
|
{
|
||||||
id: { type: String, required: true },
|
id: { type: String, required: true },
|
||||||
status: { type: Boolean, required: true },
|
status: { type: Boolean, required: true },
|
||||||
|
responseTime: { type: Number },
|
||||||
|
timings: { type: snapshotTimingsSchema },
|
||||||
|
statusCode: { type: Number },
|
||||||
|
message: { type: String },
|
||||||
|
cpu: { type: snapshotCpuSchema },
|
||||||
|
memory: { type: snapshotMemorySchema },
|
||||||
|
disk: { type: [snapshotDiskSchema] },
|
||||||
|
host: { type: snapshotHostSchema },
|
||||||
|
errors: { type: [snapshotErrorSchema] },
|
||||||
|
capture: { type: snapshotCaptureSchema },
|
||||||
|
net: { type: [snapshotNetworkInterfaceSchema] },
|
||||||
|
accessibility: { type: Number },
|
||||||
|
bestPractices: { type: Number },
|
||||||
|
seo: { type: Number },
|
||||||
|
performance: { type: Number },
|
||||||
|
audits: { type: snapshotAuditsSchema },
|
||||||
createdAt: { type: Date, required: true },
|
createdAt: { type: Date, required: true },
|
||||||
},
|
},
|
||||||
{ _id: false, strict: false }
|
{ _id: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const MonitorSchema = new Schema<MonitorDocument>(
|
const MonitorSchema = new Schema<MonitorDocument>(
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: [String],
|
||||||
required: true,
|
required: true,
|
||||||
default: "uptime",
|
default: ["uptime"],
|
||||||
enum: StatusPageTypes,
|
enum: StatusPageTypes,
|
||||||
},
|
},
|
||||||
companyName: {
|
companyName: {
|
||||||
@@ -100,6 +100,10 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
showInfrastructure: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
customCSS: {
|
customCSS: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import type { NextFunction, Request, Response } from "express";
|
import type { NextFunction, Request, Response } from "express";
|
||||||
import { logger } from "@/utils/logger.js";
|
import { logger } from "@/utils/logger.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
const handleErrors = (error: any, req: Request, res: Response, next: NextFunction) => {
|
const handleErrors = (error: unknown, req: Request, res: Response, _next: NextFunction) => {
|
||||||
const status = error.status || 500;
|
const status = error instanceof AppError ? error.status || 500 : 500;
|
||||||
const message = error.message || "Server error";
|
const message = error instanceof AppError ? error.message : "Server error";
|
||||||
const service = error.service || "unknownService";
|
const service = error instanceof AppError ? error.service : "unknownService";
|
||||||
|
const method = error instanceof AppError ? error.method : "unknownMethod";
|
||||||
logger.error({
|
logger.error({
|
||||||
message: message,
|
message: message,
|
||||||
service: service,
|
service: service,
|
||||||
method: error.method,
|
method: method,
|
||||||
stack: error.stack,
|
stack: error instanceof AppError ? error.stack : undefined,
|
||||||
|
details: error instanceof AppError ? error.details : undefined,
|
||||||
});
|
});
|
||||||
res.status(status).json({
|
res.status(status).json({
|
||||||
status,
|
status,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { Request, Response, NextFunction } from "express";
|
import type { Request, Response, NextFunction } from "express";
|
||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
|
|
||||||
export const sanitizeInput = (input: any, options = {}) => {
|
export function sanitizeInput(input: string, options?: object): string;
|
||||||
|
export function sanitizeInput(input: unknown, options?: object): unknown;
|
||||||
|
export function sanitizeInput(input: unknown, options = {}): unknown {
|
||||||
if (typeof input !== "string") {
|
if (typeof input !== "string") {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
@@ -15,9 +17,9 @@ export const sanitizeInput = (input: any, options = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return DOMPurify.sanitize(input, defaultConfig);
|
return DOMPurify.sanitize(input, defaultConfig);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const sanitizeObject = (obj: Record<string, any>, options = {}): Record<string, any> => {
|
export const sanitizeObject = (obj: unknown, options = {}): unknown => {
|
||||||
if (typeof obj !== "object" || obj === null) {
|
if (typeof obj !== "object" || obj === null) {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
@@ -26,7 +28,7 @@ export const sanitizeObject = (obj: Record<string, any>, options = {}): Record<s
|
|||||||
return obj.map((item) => sanitizeObject(item, options));
|
return obj.map((item) => sanitizeObject(item, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitized: Record<string, any> = {};
|
const sanitized: Record<string, unknown> = {};
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
sanitized[key] = sanitizeInput(value, options);
|
sanitized[key] = sanitizeInput(value, options);
|
||||||
@@ -52,7 +54,12 @@ export const sanitizeBody = (options = {}): ((req: Request, res: Response, next:
|
|||||||
export const sanitizeQuery = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => {
|
export const sanitizeQuery = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => {
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
if (req.query && typeof req.query === "object") {
|
if (req.query && typeof req.query === "object") {
|
||||||
req.query = sanitizeObject(req.query, options);
|
for (const key of Object.keys(req.query)) {
|
||||||
|
const value = req.query[key];
|
||||||
|
if (typeof value === "string") {
|
||||||
|
req.query[key] = sanitizeInput(value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ import { NextFunction, Request, Response } from "express";
|
|||||||
|
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { Settings } from "@/types/settings.js";
|
import type { ISettingsService } from "@/service/system/settingsService.js";
|
||||||
|
import type { User } from "@/types/user.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "verifyJWT";
|
const SERVICE_NAME = "verifyJWT";
|
||||||
const TOKEN_PREFIX = "Bearer ";
|
const TOKEN_PREFIX = "Bearer ";
|
||||||
|
|
||||||
export const createVerifyJWT = (settingsService: { getSettings: () => Settings }) => {
|
const isUser = (payload: unknown): payload is User => {
|
||||||
|
return typeof payload === "object" && payload !== null && "id" in payload && "teamId" in payload && "role" in payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createVerifyJWT = (settingsService: ISettingsService) => {
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
const token = req.headers["authorization"];
|
const token = req.headers["authorization"];
|
||||||
// Make sure a token is provided
|
// Make sure a token is provided
|
||||||
@@ -31,21 +36,22 @@ export const createVerifyJWT = (settingsService: { getSettings: () => Settings }
|
|||||||
next(error);
|
next(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
jwt.verify(parsedToken, jwtSecret, (err: any, decoded: any) => {
|
jwt.verify(parsedToken, jwtSecret, (err: jwt.VerifyErrors | null, decoded: jwt.JwtPayload | string | undefined) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
const error = new AppError({
|
const error = new AppError({
|
||||||
message: "Failed to authenticate token",
|
message: err instanceof Error ? err.message : "Failed to authenticate token",
|
||||||
details: err,
|
|
||||||
status: 401,
|
status: 401,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
|
details: err instanceof Error ? { error: err } : undefined,
|
||||||
method: "verifyJWT",
|
method: "verifyJWT",
|
||||||
});
|
});
|
||||||
next(error);
|
next(error);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else if (isUser(decoded)) {
|
||||||
// Token is valid, carry on
|
|
||||||
req.user = decoded;
|
req.user = decoded;
|
||||||
next();
|
next();
|
||||||
|
} else {
|
||||||
|
next(new AppError({ message: "Invalid token payload", status: 401, service: SERVICE_NAME, method: "verifyJWT" }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
|
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
|
||||||
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
|
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
|
||||||
import type { FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
|
import type { FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
|
||||||
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
|
import { GeoCheckMetadataDocument, GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
|
||||||
import mongoose from "mongoose";
|
import mongoose, { PipelineStage } from "mongoose";
|
||||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
@@ -31,13 +31,13 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapMetadata = (metadata: any): GeoCheckMetadata => ({
|
const mapMetadata = (metadata: GeoCheckMetadataDocument): GeoCheckMetadata => ({
|
||||||
monitorId: toStringId(metadata.monitorId),
|
monitorId: toStringId(metadata.monitorId),
|
||||||
teamId: toStringId(metadata.teamId),
|
teamId: toStringId(metadata.teamId),
|
||||||
type: metadata.type,
|
type: metadata.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapResults = (results: any[]): GeoCheckResult[] => {
|
const mapResults = (results: GeoCheckResult[]): GeoCheckResult[] => {
|
||||||
if (!results || !Array.isArray(results)) {
|
if (!results || !Array.isArray(results)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -89,12 +89,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
return docs.map((doc) => this.toEntity(doc));
|
return docs.map((doc) => this.toEntity(doc));
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: `Failed to createGeoChecks: ${error.message}`,
|
message: error instanceof Error ? `Failed to createGeoChecks: ${error.message}` : "Failed to createGeoChecks",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "createGeoChecks",
|
method: "createGeoChecks",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
continents?: GeoContinent[]
|
continents?: GeoContinent[]
|
||||||
): Promise<FlatGeoChecksQueryResult> => {
|
): Promise<FlatGeoChecksQueryResult> => {
|
||||||
try {
|
try {
|
||||||
const matchStage: Record<string, any> = {
|
const matchStage: Record<string, unknown> = {
|
||||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||||
...(getDateForRange(dateRange) && {
|
...(getDateForRange(dateRange) && {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
@@ -125,22 +125,8 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
skip = page * rowsPerPage;
|
skip = page * rowsPerPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (continents && continents.length > 0) {
|
|
||||||
const pipeline: any[] = [
|
|
||||||
{ $match: matchStage },
|
|
||||||
{ $unwind: "$results" },
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
"results.location.continent": { $in: continents },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
const pipeline: any[] = [{ $match: matchStage }, { $unwind: "$results" }];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common pipeline stages for both paths
|
// Common pipeline stages for both paths
|
||||||
const pipeline: any[] = [
|
const pipeline: PipelineStage[] = [
|
||||||
{ $match: matchStage },
|
{ $match: matchStage },
|
||||||
{ $unwind: "$results" },
|
{ $unwind: "$results" },
|
||||||
// Filter by continent if specified
|
// Filter by continent if specified
|
||||||
@@ -174,7 +160,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Count pipeline
|
// Count pipeline
|
||||||
const countPipeline: any[] = [
|
const countPipeline: PipelineStage[] = [
|
||||||
{ $match: matchStage },
|
{ $match: matchStage },
|
||||||
{ $unwind: "$results" },
|
{ $unwind: "$results" },
|
||||||
...(continents && continents.length > 0
|
...(continents && continents.length > 0
|
||||||
@@ -206,12 +192,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return { geoChecksCount, geoChecks };
|
return { geoChecksCount, geoChecks };
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: `Error finding geo checks by monitor ID: ${error.message}`,
|
message: error instanceof Error ? `Error finding geo checks by monitor ID: ${error.message}` : "Error finding geo checks by monitor ID",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "findByMonitorId",
|
method: "findByMonitorId",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -227,11 +213,15 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
},
|
},
|
||||||
}).sort({ createdAt: -1 });
|
}).sort({ createdAt: -1 });
|
||||||
return docs.map(this.toEntity);
|
return docs.map(this.toEntity);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: `Error finding geo checks by monitor ID and date range: ${error.message}`,
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? `Error finding geo checks by monitor ID and date range: ${error.message}`
|
||||||
|
: "Error finding geo checks by monitor ID and date range",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "findByMonitorIdAndDateRange",
|
method: "findByMonitorIdAndDateRange",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -245,7 +235,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
continents?: GeoContinent[]
|
continents?: GeoContinent[]
|
||||||
): Promise<GroupedGeoCheck[]> => {
|
): Promise<GroupedGeoCheck[]> => {
|
||||||
try {
|
try {
|
||||||
const pipeline: any[] = [
|
const pipeline: PipelineStage[] = [
|
||||||
// Match geo checks for this monitor in date range
|
// Match geo checks for this monitor in date range
|
||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
@@ -325,11 +315,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
|
|
||||||
const results = await GeoCheckModel.aggregate(pipeline);
|
const results = await GeoCheckModel.aggregate(pipeline);
|
||||||
return results as GroupedGeoCheck[];
|
return results as GroupedGeoCheck[];
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: `Error finding grouped geo checks: ${error.message}`,
|
message: error instanceof Error ? `Error finding grouped geo checks: ${error.message}` : "Error finding grouped geo checks",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "findGroupedByMonitorIdAndDateRange",
|
method: "findGroupedByMonitorIdAndDateRange",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -341,11 +332,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||||
});
|
});
|
||||||
return result.deletedCount || 0;
|
return result.deletedCount || 0;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: `Error deleting geo checks by monitor ID: ${error.message}`,
|
message: error instanceof Error ? `Error deleting geo checks by monitor ID: ${error.message}` : "Error deleting geo checks by monitor ID",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "deleteByMonitorId",
|
method: "deleteByMonitorId",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -357,11 +349,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||||
});
|
});
|
||||||
return result.deletedCount || 0;
|
return result.deletedCount || 0;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: `Error deleting geo checks by team ID: ${error.message}`,
|
message: error instanceof Error ? `Error deleting geo checks by team ID: ${error.message}` : "Error deleting geo checks by team ID",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "deleteByTeamId",
|
method: "deleteByTeamId",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -372,11 +365,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|||||||
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||||
const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } });
|
const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } });
|
||||||
return result.deletedCount || 0;
|
return result.deletedCount || 0;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: `Error deleting orphaned geo checks: ${error.message}`,
|
message: error instanceof Error ? `Error deleting orphaned geo checks: ${error.message}` : "Error deleting orphaned geo checks",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "deleteByMonitorIdsNotIn",
|
method: "deleteByMonitorIdsNotIn",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ class MongoIncidentRepository implements IIncidentsRepository {
|
|||||||
status?: boolean;
|
status?: boolean;
|
||||||
monitorId?: string;
|
monitorId?: string;
|
||||||
resolutionType?: string;
|
resolutionType?: string;
|
||||||
}): Record<string, any> {
|
}): Record<string, unknown> {
|
||||||
const matchStage: Record<string, any> = {
|
const matchStage: Record<string, unknown> = {
|
||||||
teamId: new mongoose.Types.ObjectId(teamId),
|
teamId: new mongoose.Types.ObjectId(teamId),
|
||||||
...(status !== undefined && { status }),
|
...(status !== undefined && { status }),
|
||||||
...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }),
|
...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { MaintenanceWindow } from "@/types/index.js";
|
import type { MaintenanceWindow } from "@/types/index.js";
|
||||||
import { type MaintenanceWindowDocument, MaintenanceWindowModel } from "@/db/models/index.js";
|
import { type MaintenanceWindowDocument, MaintenanceWindowModel } from "@/db/models/index.js";
|
||||||
import { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js";
|
import { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js";
|
||||||
import mongoose from "mongoose";
|
import mongoose, { SortOrder } from "mongoose";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository {
|
class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository {
|
||||||
@@ -81,7 +81,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
|
|||||||
order?: string,
|
order?: string,
|
||||||
active?: boolean
|
active?: boolean
|
||||||
): Promise<MaintenanceWindow[]> => {
|
): Promise<MaintenanceWindow[]> => {
|
||||||
const maintenanceQuery: Record<string, any> = { teamId };
|
const maintenanceQuery: Record<string, unknown> = { teamId };
|
||||||
|
|
||||||
if (active !== undefined) maintenanceQuery.active = active;
|
if (active !== undefined) maintenanceQuery.active = active;
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
const sort: Record<string, any> = {};
|
const sort: Record<string, SortOrder> = {};
|
||||||
if (field !== undefined && order !== undefined) {
|
if (field !== undefined && order !== undefined) {
|
||||||
sort[field] = order === "asc" ? 1 : -1;
|
sort[field] = order === "asc" ? 1 : -1;
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,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, unknown> = { teamId };
|
||||||
|
|
||||||
if (active !== undefined) maintenanceQuery.active = active;
|
if (active !== undefined) maintenanceQuery.active = active;
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface IMonitorsRepository {
|
|||||||
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>;
|
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>;
|
||||||
findGroupsByTeamId(teamId: string): Promise<string[]>;
|
findGroupsByTeamId(teamId: string): Promise<string[]>;
|
||||||
removeNotificationFromMonitors(notificationId: string): Promise<void>;
|
removeNotificationFromMonitors(notificationId: string): Promise<void>;
|
||||||
|
updateNotifications(teamId: string, monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set"): Promise<number>;
|
||||||
deleteByTeamIdsNotIn(teamIds: string[]): Promise<number>;
|
deleteByTeamIdsNotIn(teamIds: string[]): Promise<number>;
|
||||||
findAllMonitorIds(): Promise<string[]>;
|
findAllMonitorIds(): Promise<string[]>;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { MonitorModel } from "@/db/models/index.js";
|
import { MonitorModel } from "@/db/models/index.js";
|
||||||
import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js";
|
import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js";
|
||||||
import type { Monitor, MonitorsSummary, CheckSnapshot, Check } from "@/types/index.js";
|
import type { Monitor, MonitorsSummary, CheckSnapshot } from "@/types/index.js";
|
||||||
import mongoose, { type FilterQuery, type PipelineStage } from "mongoose";
|
import mongoose, { type FilterQuery, type PipelineStage } from "mongoose";
|
||||||
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
|
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
|
||||||
|
import { MongoBulkWriteError } from "mongodb";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
class MongoMonitorsRepository implements IMonitorsRepository {
|
class MongoMonitorsRepository implements IMonitorsRepository {
|
||||||
@@ -20,12 +21,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
try {
|
try {
|
||||||
const inserted = await MonitorModel.insertMany(payload, { ordered: false });
|
const inserted = await MonitorModel.insertMany(payload, { ordered: false });
|
||||||
return this.mapDocuments(inserted);
|
return this.mapDocuments(inserted);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
if (error.name === "MongoBulkWriteError" || error.name === "BulkWriteError") {
|
if (error instanceof MongoBulkWriteError && "insertedDocs" in error && Array.isArray(error.insertedDocs) && error.insertedDocs.length > 0) {
|
||||||
const insertedDocs = error.insertedDocs || [];
|
return this.mapDocuments(error.insertedDocs);
|
||||||
if (insertedDocs.length > 0) {
|
|
||||||
return this.mapDocuments(insertedDocs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -46,7 +44,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
};
|
};
|
||||||
|
|
||||||
findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => {
|
findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => {
|
||||||
const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type, limit } = config ?? {};
|
const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type } = config ?? {};
|
||||||
|
|
||||||
const query: Record<string, unknown> = {
|
const query: Record<string, unknown> = {
|
||||||
teamId: new mongoose.Types.ObjectId(teamId),
|
teamId: new mongoose.Types.ObjectId(teamId),
|
||||||
@@ -298,6 +296,41 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } });
|
await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateNotifications = async (
|
||||||
|
teamId: string,
|
||||||
|
monitorIds: string[],
|
||||||
|
notificationIds: string[],
|
||||||
|
action: "add" | "remove" | "set"
|
||||||
|
): Promise<number> => {
|
||||||
|
let objectIds;
|
||||||
|
let notificationObjectIds;
|
||||||
|
try {
|
||||||
|
objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||||
|
notificationObjectIds = notificationIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||||
|
} catch {
|
||||||
|
throw new AppError({ message: "One or more monitor or notification IDs are invalid", status: 400 });
|
||||||
|
}
|
||||||
|
const filter = { _id: { $in: objectIds }, teamId: new mongoose.Types.ObjectId(teamId) };
|
||||||
|
|
||||||
|
let update;
|
||||||
|
switch (action) {
|
||||||
|
case "add":
|
||||||
|
update = { $addToSet: { notifications: { $each: notificationObjectIds } } };
|
||||||
|
break;
|
||||||
|
case "remove":
|
||||||
|
update = { $pull: { notifications: { $in: notificationObjectIds } } };
|
||||||
|
break;
|
||||||
|
case "set":
|
||||||
|
update = { $set: { notifications: notificationObjectIds } };
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AppError({ message: `Invalid action: ${action}`, status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await MonitorModel.updateMany(filter, update);
|
||||||
|
return result.modifiedCount;
|
||||||
|
};
|
||||||
|
|
||||||
private mapDocuments = (documents: MonitorDocument[]): Monitor[] => {
|
private mapDocuments = (documents: MonitorDocument[]): Monitor[] => {
|
||||||
if (!documents?.length) {
|
if (!documents?.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -354,7 +387,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
gameId: doc.gameId ?? undefined,
|
gameId: doc.gameId ?? undefined,
|
||||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||||
group: doc.group ?? null,
|
group: doc.group ?? null,
|
||||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
|
||||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||||
geoCheckLocations: doc.geoCheckLocations ?? [],
|
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||||
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
||||||
@@ -363,7 +396,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
private toEntityWithChecks = (doc: any): Monitor => {
|
private toEntityWithChecks = (doc: MonitorDocument): Monitor => {
|
||||||
const toStringId = (value: unknown): string => {
|
const toStringId = (value: unknown): string => {
|
||||||
if (value instanceof mongoose.Types.ObjectId) {
|
if (value instanceof mongoose.Types.ObjectId) {
|
||||||
return value.toString();
|
return value.toString();
|
||||||
@@ -413,7 +446,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
gameId: doc.gameId ?? undefined,
|
gameId: doc.gameId ?? undefined,
|
||||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||||
group: doc.group ?? null,
|
group: doc.group ?? null,
|
||||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
|
||||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||||
geoCheckLocations: doc.geoCheckLocations ?? [],
|
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||||
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Settings } from "@/types/index.js";
|
import type { Settings, SettingsUpdate } from "@/types/index.js";
|
||||||
export interface ISettingsRepository {
|
export interface ISettingsRepository {
|
||||||
// create
|
// create
|
||||||
create(settings: Partial<Settings>): Promise<Settings>;
|
create(settings: Partial<Settings>): Promise<Settings>;
|
||||||
// fetch
|
// fetch
|
||||||
findSingleton(): Promise<Settings | null>;
|
findSingleton(): Promise<Settings | null>;
|
||||||
// update
|
// update
|
||||||
update(settings: Partial<Settings>): Promise<Settings>;
|
update(settings: SettingsUpdate): Promise<Settings>;
|
||||||
// delete
|
// delete
|
||||||
deleteLegacy: () => Promise<boolean>;
|
deleteLegacy: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import mongoose from "mongoose";
|
import mongoose, { type UpdateQuery } from "mongoose";
|
||||||
import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js";
|
import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js";
|
||||||
import type { Settings } from "@/types/index.js";
|
import type { Settings, SettingsUpdate } from "@/types/index.js";
|
||||||
import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js";
|
import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js";
|
||||||
|
|
||||||
class MongoSettingsRepository implements ISettingsRepository {
|
class MongoSettingsRepository implements ISettingsRepository {
|
||||||
@@ -59,27 +59,23 @@ class MongoSettingsRepository implements ISettingsRepository {
|
|||||||
return this.toEntity(settings);
|
return this.toEntity(settings);
|
||||||
};
|
};
|
||||||
|
|
||||||
update = async (settings: Partial<Settings>) => {
|
update = async (settings: SettingsUpdate) => {
|
||||||
const update: Record<string, any> = { $set: {}, $unset: {} };
|
const $set: Record<string, unknown> = {};
|
||||||
|
const $unset: Record<string, string> = {};
|
||||||
|
|
||||||
// Iterate through settings and separate into $set and $unset
|
// Iterate through settings and separate into $set and $unset
|
||||||
Object.entries(settings).forEach(([key, value]) => {
|
Object.entries(settings).forEach(([key, value]) => {
|
||||||
if (value === undefined || value === "") {
|
if (value === undefined || value === null) {
|
||||||
// Unset undefined or empty string values
|
$unset[key] = "";
|
||||||
update.$unset[key] = "";
|
|
||||||
} else {
|
} else {
|
||||||
// Set defined values
|
$set[key] = value;
|
||||||
update.$set[key] = value;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up empty operators
|
const update: UpdateQuery<AppSettingsDocument> = {
|
||||||
if (Object.keys(update.$set).length === 0) {
|
...(Object.keys($set).length > 0 && { $set }),
|
||||||
delete update.$set;
|
...(Object.keys($unset).length > 0 && { $unset }),
|
||||||
}
|
};
|
||||||
if (Object.keys(update.$unset).length === 0) {
|
|
||||||
delete update.$unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
await AppSettingsModel.findOneAndUpdate({}, update, {
|
await AppSettingsModel.findOneAndUpdate({}, update, {
|
||||||
upsert: true,
|
upsert: true,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
|
|||||||
showCharts: doc.showCharts,
|
showCharts: doc.showCharts,
|
||||||
showUptimePercentage: doc.showUptimePercentage,
|
showUptimePercentage: doc.showUptimePercentage,
|
||||||
showAdminLoginLink: doc.showAdminLoginLink,
|
showAdminLoginLink: doc.showAdminLoginLink,
|
||||||
|
showInfrastructure: doc.showInfrastructure,
|
||||||
customCSS: doc.customCSS,
|
customCSS: doc.customCSS,
|
||||||
createdAt: this.toDateString(doc.createdAt),
|
createdAt: this.toDateString(doc.createdAt),
|
||||||
updatedAt: this.toDateString(doc.updatedAt),
|
updatedAt: this.toDateString(doc.updatedAt),
|
||||||
@@ -71,7 +72,8 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
|
|||||||
};
|
};
|
||||||
|
|
||||||
create = async (userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => {
|
create = async (userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => {
|
||||||
const { logo: _logo, ...restData } = data;
|
const { logo, ...restData } = data;
|
||||||
|
void logo;
|
||||||
const statusPage = new StatusPageModel({
|
const statusPage = new StatusPageModel({
|
||||||
...restData,
|
...restData,
|
||||||
userId,
|
userId,
|
||||||
@@ -109,7 +111,8 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
|
|||||||
image: Express.Multer.File | undefined,
|
image: Express.Multer.File | undefined,
|
||||||
patch: Partial<StatusPage> & { removeLogo?: string }
|
patch: Partial<StatusPage> & { removeLogo?: string }
|
||||||
): Promise<StatusPage> => {
|
): Promise<StatusPage> => {
|
||||||
const { logo: _logo, removeLogo, ...restPatch } = patch;
|
const { logo, removeLogo, ...restPatch } = patch;
|
||||||
|
void logo;
|
||||||
const updateData: StatusPageUpdateData = { ...restPatch };
|
const updateData: StatusPageUpdateData = { ...restPatch };
|
||||||
if (image) {
|
if (image) {
|
||||||
updateData.logo = {
|
updateData.logo = {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class MongoUsersRepository implements IUsersRepository {
|
|||||||
|
|
||||||
delete candidateUser.deleteProfileImage;
|
delete candidateUser.deleteProfileImage;
|
||||||
|
|
||||||
const updateQuery: Record<string, any> = { $set: candidateUser };
|
const updateQuery: Record<string, unknown> = { $set: candidateUser };
|
||||||
if (unsetFields) {
|
if (unsetFields) {
|
||||||
updateQuery.$unset = unsetFields;
|
updateQuery.$unset = unsetFields;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Router, RequestHandler } from "express";
|
import { Router, RequestHandler } from "express";
|
||||||
import { isAllowed } from "../middleware/isAllowed.js";
|
import { isAllowed } from "../middleware/isAllowed.js";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
|
import { IAuthController } from "@/controllers/authController.js";
|
||||||
|
|
||||||
const upload = multer();
|
const upload = multer();
|
||||||
|
|
||||||
class AuthRoutes {
|
class AuthRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private authController: any;
|
private authController: IAuthController;
|
||||||
|
|
||||||
constructor(authController: any, verifyJWT: RequestHandler) {
|
constructor(authController: IAuthController, verifyJWT: RequestHandler) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.authController = authController;
|
this.authController = authController;
|
||||||
this.initRoutes(verifyJWT);
|
this.initRoutes(verifyJWT);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
|
||||||
import { isAllowed } from "../middleware/isAllowed.js";
|
import { isAllowed } from "../middleware/isAllowed.js";
|
||||||
|
import { ICheckController } from "@/controllers/checkController.js";
|
||||||
|
|
||||||
class CheckRoutes {
|
class CheckRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private checkController: any;
|
private checkController: ICheckController;
|
||||||
|
|
||||||
constructor(checkController: any) {
|
constructor(checkController: ICheckController) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.checkController = checkController;
|
this.checkController = checkController;
|
||||||
this.initRoutes();
|
this.initRoutes();
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { RequestHandler, Router } from "express";
|
import { RequestHandler, Router } from "express";
|
||||||
import { isAllowed } from "../middleware/isAllowed.js";
|
import { isAllowed } from "../middleware/isAllowed.js";
|
||||||
|
import { IDiagnosticController } from "@/controllers/diagnosticController.js";
|
||||||
|
|
||||||
class DiagnosticRoutes {
|
class DiagnosticRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private diagnosticController: any;
|
private diagnosticController: IDiagnosticController;
|
||||||
|
|
||||||
constructor(diagnosticController: any, verifyJWT: RequestHandler) {
|
constructor(diagnosticController: IDiagnosticController, verifyJWT: RequestHandler) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.diagnosticController = diagnosticController;
|
this.diagnosticController = diagnosticController;
|
||||||
this.initRoutes(verifyJWT);
|
this.initRoutes(verifyJWT);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { IGeoCheckController } from "@/controllers/geoCheckController.js";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
|
||||||
class GeoCheckRoutes {
|
class GeoCheckRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private geoCheckController: any;
|
private geoCheckController: IGeoCheckController;
|
||||||
|
|
||||||
constructor(geoCheckController: any) {
|
constructor(geoCheckController: IGeoCheckController) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.geoCheckController = geoCheckController;
|
this.geoCheckController = geoCheckController;
|
||||||
this.initRoutes();
|
this.initRoutes();
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { isAllowed } from "../middleware/isAllowed.js";
|
import { isAllowed } from "../middleware/isAllowed.js";
|
||||||
|
import { IIncidentController } from "@/controllers/incidentController.js";
|
||||||
|
|
||||||
class IncidentRoutes {
|
class IncidentRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private incidentController: any;
|
private incidentController: IIncidentController;
|
||||||
|
|
||||||
constructor(incidentController: any) {
|
constructor(incidentController: IIncidentController) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.incidentController = incidentController;
|
this.incidentController = incidentController;
|
||||||
this.initRoutes();
|
this.initRoutes();
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { RequestHandler, Router } from "express";
|
import { RequestHandler, Router } from "express";
|
||||||
import { isAllowed } from "../middleware/isAllowed.js";
|
import { isAllowed } from "../middleware/isAllowed.js";
|
||||||
|
import { IInviteController } from "@/controllers/inviteController.js";
|
||||||
|
|
||||||
class InviteRoutes {
|
class InviteRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private inviteController: any;
|
private inviteController: IInviteController;
|
||||||
|
|
||||||
constructor(inviteController: any, verifyJWT: RequestHandler) {
|
constructor(inviteController: IInviteController, verifyJWT: RequestHandler) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.inviteController = inviteController;
|
this.inviteController = inviteController;
|
||||||
this.initRoutes(verifyJWT);
|
this.initRoutes(verifyJWT);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { isAllowed } from "../middleware/isAllowed.js";
|
import { isAllowed } from "@/middleware/isAllowed.js";
|
||||||
|
import { ILogController } from "@/controllers/logController.js";
|
||||||
class LogRoutes {
|
class LogRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private logController: any;
|
private logController: ILogController;
|
||||||
|
|
||||||
constructor(logController: any) {
|
constructor(logController: ILogController) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.logController = logController;
|
this.logController = logController;
|
||||||
this.initRoutes();
|
this.initRoutes();
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { IMaintenanceWindowController } from "@/controllers/maintenanceWindowController.js";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
|
||||||
class MaintenanceWindowRoutes {
|
class MaintenanceWindowRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private mwController: any;
|
private mwController: IMaintenanceWindowController;
|
||||||
|
|
||||||
constructor(maintenanceWindowController: any) {
|
constructor(maintenanceWindowController: IMaintenanceWindowController) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.mwController = maintenanceWindowController;
|
this.mwController = maintenanceWindowController;
|
||||||
this.initRoutes();
|
this.initRoutes();
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { isAllowed } from "@/middleware/isAllowed.js";
|
import { isAllowed } from "@/middleware/isAllowed.js";
|
||||||
import multer from "multer";
|
import { IMonitorController } from "@/controllers/monitorController.js";
|
||||||
import { fetchMonitorCertificate } from "@/controllers/controllerUtils.js";
|
|
||||||
const upload = multer({
|
|
||||||
storage: multer.memoryStorage(), // Store file in memory as Buffer
|
|
||||||
});
|
|
||||||
|
|
||||||
class MonitorRoutes {
|
class MonitorRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private monitorController: any;
|
private monitorController: IMonitorController;
|
||||||
constructor(monitorController: any) {
|
constructor(monitorController: IMonitorController) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.monitorController = monitorController;
|
this.monitorController = monitorController;
|
||||||
this.initRoutes();
|
this.initRoutes();
|
||||||
@@ -38,10 +34,11 @@ class MonitorRoutes {
|
|||||||
|
|
||||||
// Util routes
|
// Util routes
|
||||||
this.router.get("/certificate/:monitorId", (req, res, next) => {
|
this.router.get("/certificate/:monitorId", (req, res, next) => {
|
||||||
this.monitorController.getMonitorCertificate(req, res, next, fetchMonitorCertificate);
|
this.monitorController.getMonitorCertificate(req, res, next);
|
||||||
});
|
});
|
||||||
|
|
||||||
// General monitor CRUD routes
|
// General monitor CRUD routes
|
||||||
|
this.router.patch("/notifications", isAllowed(["admin", "superadmin"]), this.monitorController.updateNotifications);
|
||||||
this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor);
|
this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor);
|
||||||
this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors);
|
this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { INotificationController } from "@/controllers/notificationController.js";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
class NotificationRoutes {
|
class NotificationRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private notificationController: any;
|
private notificationController: INotificationController;
|
||||||
|
|
||||||
constructor(notificationController: any) {
|
constructor(notificationController: INotificationController) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.notificationController = notificationController;
|
this.notificationController = notificationController;
|
||||||
this.initializeRoutes();
|
this.initializeRoutes();
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { isAllowed } from "../middleware/isAllowed.js";
|
import { isAllowed } from "@/middleware/isAllowed.js";
|
||||||
|
import { IJobQueueController } from "@/controllers/queueController.js";
|
||||||
class QueueRoutes {
|
class QueueRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private queueController: any;
|
private queueController: IJobQueueController;
|
||||||
|
|
||||||
constructor(queueController: any) {
|
constructor(queueController: IJobQueueController) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.queueController = queueController;
|
this.queueController = queueController;
|
||||||
this.initRoutes();
|
this.initRoutes();
|
||||||
}
|
}
|
||||||
initRoutes() {
|
initRoutes() {
|
||||||
this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs);
|
this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs);
|
||||||
this.router.post("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.addJob);
|
|
||||||
|
|
||||||
this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics);
|
this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics);
|
||||||
this.router.get("/health", isAllowed(["admin", "superadmin"]), this.queueController.checkQueueHealth);
|
|
||||||
this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics);
|
this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics);
|
||||||
this.router.post("/flush", isAllowed(["admin", "superadmin"]), this.queueController.flushQueue);
|
this.router.post("/flush", isAllowed(["admin", "superadmin"]), this.queueController.flushQueue);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { isAllowed } from "../middleware/isAllowed.js";
|
import { isAllowed } from "../middleware/isAllowed.js";
|
||||||
|
import { ISettingsController } from "@/controllers/settingsController.js";
|
||||||
|
|
||||||
class SettingsRoutes {
|
class SettingsRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private settingsController: any;
|
private settingsController: ISettingsController;
|
||||||
|
|
||||||
constructor(settingsController: any) {
|
constructor(settingsController: ISettingsController) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.settingsController = settingsController;
|
this.settingsController = settingsController;
|
||||||
this.initRoutes();
|
this.initRoutes();
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import { IStatusPageController } from "@/controllers/statusPageController.js";
|
||||||
import { RequestHandler, Router } from "express";
|
import { RequestHandler, Router } from "express";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
const upload = multer();
|
const upload = multer();
|
||||||
|
|
||||||
class StatusPageRoutes {
|
class StatusPageRoutes {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
private statusPageController: any;
|
private statusPageController: IStatusPageController;
|
||||||
|
|
||||||
constructor(statusPageController: any, verifyJWT: RequestHandler, verifyStatusPageAccess: RequestHandler) {
|
constructor(statusPageController: IStatusPageController, verifyJWT: RequestHandler, verifyStatusPageAccess: RequestHandler) {
|
||||||
this.router = Router();
|
this.router = Router();
|
||||||
this.statusPageController = statusPageController;
|
this.statusPageController = statusPageController;
|
||||||
this.initRoutes(verifyJWT, verifyStatusPageAccess);
|
this.initRoutes(verifyJWT, verifyStatusPageAccess);
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ export class DiagnosticService implements IDiagnosticService {
|
|||||||
* Clears performance marks after each measurement to prevent memory leaks.
|
* Clears performance marks after each measurement to prevent memory leaks.
|
||||||
*/
|
*/
|
||||||
const obs = new PerformanceObserver((items) => {
|
const obs = new PerformanceObserver((items) => {
|
||||||
// Get the first entry but we don't need to store it
|
items.getEntries();
|
||||||
items.getEntries()[0];
|
|
||||||
performance.clearMarks();
|
performance.clearMarks();
|
||||||
});
|
});
|
||||||
obs.observe({ entryTypes: ["measure"] });
|
obs.observe({ entryTypes: ["measure"] });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Monitor, GeoCheck } from "@/types/index.js";
|
import type { Monitor, GeoCheck } from "@/types/index.js";
|
||||||
import type { GeoCheckResult } from "@/types/geoCheck.js";
|
import type { GeoCheckResult, GeoContinent } from "@/types/geoCheck.js";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import type { IGeoChecksRepository } from "@/repositories/index.js";
|
import type { FlatGeoChecksQueryResult, IGeoChecksRepository } from "@/repositories/index.js";
|
||||||
import type { IMonitorsRepository } from "@/repositories/index.js";
|
import type { IMonitorsRepository } from "@/repositories/index.js";
|
||||||
import type { IGlobalPingService } from "@/service/infrastructure/globalPingService.js";
|
import type { IGlobalPingService } from "@/service/infrastructure/globalPingService.js";
|
||||||
import type { ILogger } from "@/utils/logger.js";
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
@@ -13,7 +13,15 @@ export interface IGeoChecksService {
|
|||||||
readonly serviceName: string;
|
readonly serviceName: string;
|
||||||
buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null>;
|
buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null>;
|
||||||
createGeoChecks(geoChecks: GeoCheck[]): Promise<GeoCheck[]>;
|
createGeoChecks(geoChecks: GeoCheck[]): Promise<GeoCheck[]>;
|
||||||
getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise<any>;
|
getGeoChecksByMonitor(args: {
|
||||||
|
monitorId: string;
|
||||||
|
teamId: string;
|
||||||
|
sortOrder: string;
|
||||||
|
dateRange: string;
|
||||||
|
page?: number;
|
||||||
|
rowsPerPage?: number;
|
||||||
|
continent: GeoContinent | GeoContinent[];
|
||||||
|
}): Promise<FlatGeoChecksQueryResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GeoChecksService implements IGeoChecksService {
|
export class GeoChecksService implements IGeoChecksService {
|
||||||
@@ -139,7 +147,23 @@ export class GeoChecksService implements IGeoChecksService {
|
|||||||
return this.geoChecksRepository.createGeoChecks(geoChecks);
|
return this.geoChecksRepository.createGeoChecks(geoChecks);
|
||||||
};
|
};
|
||||||
|
|
||||||
getGeoChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => {
|
getGeoChecksByMonitor = async ({
|
||||||
|
monitorId,
|
||||||
|
teamId,
|
||||||
|
sortOrder,
|
||||||
|
dateRange,
|
||||||
|
page,
|
||||||
|
rowsPerPage,
|
||||||
|
continent,
|
||||||
|
}: {
|
||||||
|
monitorId: string;
|
||||||
|
teamId: string;
|
||||||
|
sortOrder: string;
|
||||||
|
dateRange: string;
|
||||||
|
page?: number;
|
||||||
|
rowsPerPage?: number;
|
||||||
|
continent: GeoContinent | GeoContinent[];
|
||||||
|
}) => {
|
||||||
if (!monitorId) {
|
if (!monitorId) {
|
||||||
throw new AppError({
|
throw new AppError({
|
||||||
message: "No monitor ID in request",
|
message: "No monitor ID in request",
|
||||||
@@ -167,10 +191,9 @@ export class GeoChecksService implements IGeoChecksService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
const parsedPage = page ? parseInt(page) : page;
|
const parsedPage = page || 0;
|
||||||
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
|
const parsedRowsPerPage = rowsPerPage || 5;
|
||||||
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import type { Invite, UserRole } from "@/types/index.js";
|
|||||||
import { canManageRole } from "@/types/user.js";
|
import { canManageRole } from "@/types/user.js";
|
||||||
import type { IInvitesRepository } from "@/repositories/index.js";
|
import type { IInvitesRepository } from "@/repositories/index.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import { ISettingsService } from "../system/settingsService.js";
|
||||||
|
import { IEmailService } from "../infrastructure/emailService.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "inviteService";
|
const SERVICE_NAME = "inviteService";
|
||||||
|
|
||||||
@@ -14,8 +16,8 @@ export interface IInviteService {
|
|||||||
export class InviteService implements IInviteService {
|
export class InviteService implements IInviteService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private settingsService: any;
|
private settingsService: ISettingsService;
|
||||||
private emailService: any;
|
private emailService: IEmailService;
|
||||||
private invitesRepository: IInvitesRepository;
|
private invitesRepository: IInvitesRepository;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
@@ -24,8 +26,8 @@ export class InviteService implements IInviteService {
|
|||||||
emailService,
|
emailService,
|
||||||
}: {
|
}: {
|
||||||
invitesRepository: IInvitesRepository;
|
invitesRepository: IInvitesRepository;
|
||||||
settingsService: any;
|
settingsService: ISettingsService;
|
||||||
emailService: any;
|
emailService: IEmailService;
|
||||||
}) {
|
}) {
|
||||||
this.invitesRepository = invitesRepository;
|
this.invitesRepository = invitesRepository;
|
||||||
this.settingsService = settingsService;
|
this.settingsService = settingsService;
|
||||||
@@ -57,8 +59,16 @@ export class InviteService implements IInviteService {
|
|||||||
return inviteToken;
|
return inviteToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
sendInviteEmail = async ({ invite, firstName, userRoles }: { invite: Partial<Invite>; firstName: any; userRoles: UserRole[] }) => {
|
sendInviteEmail = async ({ invite, firstName, userRoles }: { invite: Partial<Invite>; firstName: string; userRoles: UserRole[] }) => {
|
||||||
const inviteRoles = invite.role ?? [];
|
const inviteRoles = invite.role ?? [];
|
||||||
|
if (!invite.email) {
|
||||||
|
throw new AppError({
|
||||||
|
message: "Invite email is required to send an invite",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "sendInviteEmail",
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (const targetRole of inviteRoles) {
|
for (const targetRole of inviteRoles) {
|
||||||
const canManage = userRoles.some((actorRole) => canManageRole(actorRole, targetRole));
|
const canManage = userRoles.some((actorRole) => canManageRole(actorRole, targetRole));
|
||||||
@@ -66,7 +76,7 @@ export class InviteService implements IInviteService {
|
|||||||
throw new AppError({
|
throw new AppError({
|
||||||
message: "You do not have permission to create this invite",
|
message: "You do not have permission to create this invite",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getInviteToken",
|
method: "sendInviteEmail",
|
||||||
status: 403,
|
status: 403,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -79,6 +89,16 @@ export class InviteService implements IInviteService {
|
|||||||
name: firstName,
|
name: firstName,
|
||||||
link: `${clientHost}/register/${inviteToken.token}`,
|
link: `${clientHost}/register/${inviteToken.token}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
throw new AppError({
|
||||||
|
message: "Failed to build invite e-mail... Please verify your settings.",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "sendInviteEmail",
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result = await this.emailService.sendEmail(invite.email, "Welcome to Uptime Monitor", html);
|
const result = await this.emailService.sendEmail(invite.email, "Welcome to Uptime Monitor", html);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new AppError({
|
throw new AppError({
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
|
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||||
import type { MaintenanceWindow } from "@/types/index.js";
|
import type { DurationUnit, 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";
|
||||||
|
|
||||||
export interface IMaintenanceWindowService {
|
export interface IMaintenanceWindowService {
|
||||||
createMaintenanceWindow(params: { teamId: string; body: Partial<MaintenanceWindow> & { monitors: string[] } }): Promise<void>;
|
createMaintenanceWindow(params: {
|
||||||
|
teamId: string;
|
||||||
|
monitorIDs: string[];
|
||||||
|
name: string;
|
||||||
|
active: boolean;
|
||||||
|
duration: number;
|
||||||
|
durationUnit: DurationUnit;
|
||||||
|
repeat: number;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
}): Promise<void>;
|
||||||
getMaintenanceWindowById(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
|
getMaintenanceWindowById(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
|
||||||
getMaintenanceWindowsByTeamId(params: {
|
getMaintenanceWindowsByTeamId(params: {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -40,9 +50,28 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
|
|||||||
return MaintenanceWindowService.SERVICE_NAME;
|
return MaintenanceWindowService.SERVICE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
createMaintenanceWindow = async ({ teamId, body }: { teamId: string; body: any }) => {
|
createMaintenanceWindow = async ({
|
||||||
const monitorIds = body.monitors;
|
teamId,
|
||||||
const monitors = await this.monitorsRepository.findByIds(monitorIds);
|
monitorIDs,
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
duration,
|
||||||
|
durationUnit,
|
||||||
|
repeat,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
monitorIDs: string[];
|
||||||
|
name: string;
|
||||||
|
active: boolean;
|
||||||
|
duration: number;
|
||||||
|
durationUnit: DurationUnit;
|
||||||
|
repeat: number;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
}) => {
|
||||||
|
const monitors = await this.monitorsRepository.findByIds(monitorIDs);
|
||||||
|
|
||||||
const unauthorizedMonitors = monitors.filter((monitor) => monitor.teamId !== teamId);
|
const unauthorizedMonitors = monitors.filter((monitor) => monitor.teamId !== teamId);
|
||||||
|
|
||||||
@@ -55,17 +84,17 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbTransactions = monitorIds.map((monitorId: string) => {
|
const dbTransactions = monitorIDs.map((monitorId: string) => {
|
||||||
return this.maintenanceWindowsRepository.create({
|
return this.maintenanceWindowsRepository.create({
|
||||||
teamId,
|
teamId,
|
||||||
monitorId,
|
monitorId,
|
||||||
name: body.name,
|
name: name,
|
||||||
active: body.active ? body.active : true,
|
active: active,
|
||||||
duration: body.duration,
|
duration: duration,
|
||||||
durationUnit: body.durationUnit,
|
durationUnit: durationUnit,
|
||||||
repeat: body.repeat,
|
repeat: repeat,
|
||||||
start: body.start,
|
start: start,
|
||||||
end: body.end,
|
end: end,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await Promise.all(dbTransactions);
|
await Promise.all(dbTransactions);
|
||||||
@@ -106,7 +135,7 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
|
|||||||
return await this.maintenanceWindowsRepository.deleteById(id, teamId);
|
return await this.maintenanceWindowsRepository.deleteById(id, teamId);
|
||||||
};
|
};
|
||||||
|
|
||||||
editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: any }) => {
|
editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: Partial<MaintenanceWindow> }) => {
|
||||||
return await this.maintenanceWindowsRepository.updateById(id, teamId, body);
|
return await this.maintenanceWindowsRepository.updateById(id, teamId, body);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
HardwareDetailsResult,
|
HardwareDetailsResult,
|
||||||
PageSpeedDetailsResult,
|
PageSpeedDetailsResult,
|
||||||
GamesMap,
|
GamesMap,
|
||||||
|
GroupedGeoCheckResult,
|
||||||
} from "@/types/monitor.js";
|
} from "@/types/monitor.js";
|
||||||
import { supportsGeoCheck } from "@/types/monitor.js";
|
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||||
import type { GeoContinent } from "@/types/geoCheck.js";
|
import type { GeoContinent } from "@/types/geoCheck.js";
|
||||||
@@ -20,7 +21,10 @@ import type {
|
|||||||
} from "@/repositories/index.js";
|
} from "@/repositories/index.js";
|
||||||
import demoMonitorsData from "@/utils/demoMonitors.json" with { type: "json" };
|
import demoMonitorsData from "@/utils/demoMonitors.json" with { type: "json" };
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import type { ImportedMonitor } from "@/validation/monitorValidation.js";
|
||||||
import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||||
|
import { IEmailService } from "../infrastructure/emailService.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "MonitorService";
|
const SERVICE_NAME = "MonitorService";
|
||||||
type DateRangeKey = "recent" | "day" | "week" | "month" | "all";
|
type DateRangeKey = "recent" | "day" | "week" | "month" | "all";
|
||||||
@@ -30,14 +34,19 @@ export interface IMonitorService {
|
|||||||
|
|
||||||
// create
|
// create
|
||||||
createMonitor(teamId: string, userId: string, body: Partial<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>): Promise<Monitor[] | null>;
|
||||||
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
|
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
|
||||||
|
|
||||||
// read
|
// read
|
||||||
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<UptimeDetailsResult>;
|
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<UptimeDetailsResult>;
|
||||||
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<HardwareDetailsResult>;
|
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<HardwareDetailsResult>;
|
||||||
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<PageSpeedDetailsResult>;
|
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<PageSpeedDetailsResult>;
|
||||||
getGeoChecksByMonitorId(args: { teamId: string; monitorId: string; dateRange: string; continents?: GeoContinent[] }): Promise<any>;
|
getGeoChecksByMonitorId(args: {
|
||||||
|
teamId: string;
|
||||||
|
monitorId: string;
|
||||||
|
dateRange: string;
|
||||||
|
continents?: GeoContinent[];
|
||||||
|
}): Promise<GroupedGeoCheckResult>;
|
||||||
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
||||||
getMonitorsByTeamId(args: {
|
getMonitorsByTeamId(args: {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -58,7 +67,6 @@ export interface IMonitorService {
|
|||||||
filter?: string;
|
filter?: string;
|
||||||
field?: string;
|
field?: string;
|
||||||
order?: "asc" | "desc";
|
order?: "asc" | "desc";
|
||||||
explain?: boolean;
|
|
||||||
}): Promise<MonitorsWithChecksByTeamIdResult>;
|
}): Promise<MonitorsWithChecksByTeamIdResult>;
|
||||||
getAllGames(): GamesMap;
|
getAllGames(): GamesMap;
|
||||||
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
|
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
|
||||||
@@ -71,18 +79,21 @@ export interface IMonitorService {
|
|||||||
deleteMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
deleteMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
||||||
deleteAllMonitors(args: { teamId: string }): Promise<number>;
|
deleteAllMonitors(args: { teamId: string }): Promise<number>;
|
||||||
|
|
||||||
|
// notifications
|
||||||
|
updateNotifications(args: { teamId: string; monitorIds: string[]; notificationIds: string[]; action: "add" | "remove" | "set" }): Promise<number>;
|
||||||
|
|
||||||
// other
|
// other
|
||||||
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: ImportedMonitor[] }): Promise<{ imported: number; errors: string[] }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MonitorService implements IMonitorService {
|
export class MonitorService implements IMonitorService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private jobQueue: ISuperSimpleQueue;
|
private jobQueue: ISuperSimpleQueue;
|
||||||
private emailService: any;
|
private emailService: IEmailService;
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
private games: any;
|
private games: GamesMap;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
private checksRepository: IChecksRepository;
|
private checksRepository: IChecksRepository;
|
||||||
private geoChecksRepository: IGeoChecksRepository;
|
private geoChecksRepository: IGeoChecksRepository;
|
||||||
@@ -103,9 +114,9 @@ export class MonitorService implements IMonitorService {
|
|||||||
incidentsRepository,
|
incidentsRepository,
|
||||||
}: {
|
}: {
|
||||||
jobQueue: ISuperSimpleQueue;
|
jobQueue: ISuperSimpleQueue;
|
||||||
emailService: any;
|
emailService: IEmailService;
|
||||||
logger: any;
|
logger: ILogger;
|
||||||
games: any;
|
games: GamesMap;
|
||||||
monitorsRepository: IMonitorsRepository;
|
monitorsRepository: IMonitorsRepository;
|
||||||
checksRepository: IChecksRepository;
|
checksRepository: IChecksRepository;
|
||||||
geoChecksRepository: IGeoChecksRepository;
|
geoChecksRepository: IGeoChecksRepository;
|
||||||
@@ -163,7 +174,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
this.jobQueue.addJob(monitor.id, monitor);
|
this.jobQueue.addJob(monitor.id, monitor);
|
||||||
};
|
};
|
||||||
|
|
||||||
createMonitors = async (monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null> => {
|
createMonitors = async (monitors: Array<Monitor>): Promise<Monitor[] | null> => {
|
||||||
const createdMonitors = await this.monitorsRepository.createMonitors(monitors);
|
const createdMonitors = await this.monitorsRepository.createMonitors(monitors);
|
||||||
if (!monitors || monitors.length === 0) {
|
if (!monitors || monitors.length === 0) {
|
||||||
throw new AppError({ message: "Failed to create monitors", status: 500, service: SERVICE_NAME, method: "createMonitors" });
|
throw new AppError({ message: "Failed to create monitors", status: 500, service: SERVICE_NAME, method: "createMonitors" });
|
||||||
@@ -173,7 +184,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
return createdMonitors;
|
return createdMonitors;
|
||||||
};
|
};
|
||||||
|
|
||||||
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<any[]> => {
|
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<Monitor[]> => {
|
||||||
const demoData = demoMonitorsData;
|
const demoData = demoMonitorsData;
|
||||||
const monitors = demoData.map((monitor) => ({
|
const monitors = demoData.map((monitor) => ({
|
||||||
userId,
|
userId,
|
||||||
@@ -194,12 +205,10 @@ export class MonitorService implements IMonitorService {
|
|||||||
teamId,
|
teamId,
|
||||||
monitorId,
|
monitorId,
|
||||||
dateRange,
|
dateRange,
|
||||||
normalize,
|
|
||||||
}: {
|
}: {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
monitorId: string;
|
monitorId: string;
|
||||||
dateRange: string;
|
dateRange: string;
|
||||||
normalize?: boolean;
|
|
||||||
}): Promise<UptimeDetailsResult> => {
|
}): Promise<UptimeDetailsResult> => {
|
||||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||||
if (!monitor) {
|
if (!monitor) {
|
||||||
@@ -327,7 +336,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
monitorId: string;
|
monitorId: string;
|
||||||
dateRange: string;
|
dateRange: string;
|
||||||
continents?: GeoContinent[];
|
continents?: GeoContinent[];
|
||||||
}): Promise<any> => {
|
}): Promise<GroupedGeoCheckResult> => {
|
||||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||||
if (!monitor) {
|
if (!monitor) {
|
||||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||||
@@ -380,7 +389,6 @@ export class MonitorService implements IMonitorService {
|
|||||||
filter,
|
filter,
|
||||||
field,
|
field,
|
||||||
order,
|
order,
|
||||||
explain,
|
|
||||||
}: {
|
}: {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
@@ -390,7 +398,6 @@ export class MonitorService implements IMonitorService {
|
|||||||
filter?: string;
|
filter?: string;
|
||||||
field?: string;
|
field?: string;
|
||||||
order?: "asc" | "desc";
|
order?: "asc" | "desc";
|
||||||
explain?: boolean;
|
|
||||||
}): Promise<MonitorsWithChecksByTeamIdResult> => {
|
}): Promise<MonitorsWithChecksByTeamIdResult> => {
|
||||||
const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId, { type });
|
const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId, { type });
|
||||||
const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter });
|
const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter });
|
||||||
@@ -435,49 +442,75 @@ export class MonitorService implements IMonitorService {
|
|||||||
return editedMonitor;
|
return editedMonitor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateNotifications = async ({
|
||||||
|
teamId,
|
||||||
|
monitorIds,
|
||||||
|
notificationIds,
|
||||||
|
action,
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
monitorIds: string[];
|
||||||
|
notificationIds: string[];
|
||||||
|
action: "add" | "remove" | "set";
|
||||||
|
}): Promise<number> => {
|
||||||
|
const modifiedCount = await this.monitorsRepository.updateNotifications(teamId, monitorIds, notificationIds, action);
|
||||||
|
|
||||||
|
// If notifications were updated, we should update the jobs in the queue
|
||||||
|
if (modifiedCount > 0) {
|
||||||
|
const monitors = await this.monitorsRepository.findByIds(monitorIds);
|
||||||
|
await Promise.all(monitors.map((monitor) => this.jobQueue.updateJob(monitor)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifiedCount;
|
||||||
|
};
|
||||||
|
|
||||||
pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
|
pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
|
||||||
const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId);
|
const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId);
|
||||||
monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor);
|
if (monitor.isActive) {
|
||||||
|
await this.jobQueue.resumeJob(monitor);
|
||||||
|
} else {
|
||||||
|
await this.jobQueue.pauseJob(monitor);
|
||||||
|
}
|
||||||
return monitor;
|
return monitor;
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
|
deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
|
||||||
const monitor = await this.monitorsRepository.deleteById(monitorId, teamId);
|
const monitor = await this.monitorsRepository.deleteById(monitorId, teamId);
|
||||||
await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
|
await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`,
|
message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
stack: err.stack,
|
stack: err instanceof Error ? err.stack : undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
|
await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `Error deleting checks for monitor ${monitor.id} with name ${monitor.name}`,
|
message: `Error deleting checks for monitor ${monitor.id} with name ${monitor.name}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
stack: err.stack,
|
stack: err instanceof Error ? err.stack : undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: any) => {
|
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: unknown) => {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `Error removing monitor ${monitor.id} with name ${monitor.name} from status pages`,
|
message: `Error removing monitor ${monitor.id} with name ${monitor.name} from status pages`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
stack: err.stack,
|
stack: err instanceof Error ? err.stack : undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId).catch((err: any) => {
|
await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId).catch((err: unknown) => {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `Error deleting incidents for monitor ${monitor.id} with name ${monitor.name}`,
|
message: `Error deleting incidents for monitor ${monitor.id} with name ${monitor.name}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
stack: err.stack,
|
stack: err instanceof Error ? err.stack : undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.geoChecksRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
|
await this.geoChecksRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `Error deleting geo checks for monitor ${monitor.id} with name ${monitor.name}`,
|
message: `Error deleting geo checks for monitor ${monitor.id} with name ${monitor.name}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
stack: err.stack,
|
stack: err instanceof Error ? err.stack : undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -495,12 +528,12 @@ export class MonitorService implements IMonitorService {
|
|||||||
await this.geoChecksRepository.deleteByMonitorId(monitor.id);
|
await this.geoChecksRepository.deleteByMonitorId(monitor.id);
|
||||||
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id);
|
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id);
|
||||||
await this.monitorStatsRepository.deleteByMonitorId(monitor.id);
|
await this.monitorStatsRepository.deleteByMonitorId(monitor.id);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`,
|
message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "deleteAllMonitors",
|
method: "deleteAllMonitors",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -508,7 +541,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
return deletedCount;
|
return deletedCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<any[]> => {
|
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<Monitor[]> => {
|
||||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
|
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
|
||||||
|
|
||||||
if (!monitors || monitors.length === 0) {
|
if (!monitors || monitors.length === 0) {
|
||||||
@@ -525,23 +558,21 @@ export class MonitorService implements IMonitorService {
|
|||||||
}: {
|
}: {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
monitors: any[];
|
monitors: ImportedMonitor[];
|
||||||
}): Promise<{ imported: number; errors: string[] }> => {
|
}): Promise<{ imported: number; errors: string[] }> => {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
const cleanedMonitors = monitors.map((monitor) => {
|
const cleanedMonitors: Monitor[] = monitors.map((monitor) => ({
|
||||||
const cleanData = { ...monitor };
|
...monitor,
|
||||||
delete cleanData.id;
|
id: "",
|
||||||
delete cleanData._id;
|
teamId,
|
||||||
delete cleanData.createdAt;
|
userId,
|
||||||
delete cleanData.updatedAt;
|
recentChecks: [],
|
||||||
delete cleanData.recentChecks;
|
createdAt: "",
|
||||||
// Monitors must belong to current team
|
updatedAt: "",
|
||||||
cleanData.teamId = teamId;
|
}));
|
||||||
return cleanData;
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdMonitors = await this.createMonitors(cleanedMonitors, userId, teamId);
|
const createdMonitors = await this.createMonitors(cleanedMonitors);
|
||||||
|
|
||||||
if (!createdMonitors || createdMonitors.length === 0) {
|
if (!createdMonitors || createdMonitors.length === 0) {
|
||||||
throw new AppError({
|
throw new AppError({
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export class UserService implements IUserService {
|
|||||||
user.password = this.hashPassword(user.password);
|
user.password = this.hashPassword(user.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = await this.usersRepository.create(user, file);
|
const newUser = await this.usersRepository.create(user as User, file);
|
||||||
|
|
||||||
this.logger.debug({
|
this.logger.debug({
|
||||||
message: "New user created",
|
message: "New user created",
|
||||||
@@ -207,7 +207,7 @@ export class UserService implements IUserService {
|
|||||||
userData.password = this.hashPassword(userData.password);
|
userData.password = this.hashPassword(userData.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = await this.usersRepository.create(userData, file);
|
const newUser = await this.usersRepository.create(userData as User, file);
|
||||||
|
|
||||||
this.logger.debug({
|
this.logger.debug({
|
||||||
message: "New user created by superadmin",
|
message: "New user created by superadmin",
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { Monitor } from "@/types/monitor.js";
|
|||||||
import jmespath from "jmespath";
|
import jmespath from "jmespath";
|
||||||
type JmesPath = typeof jmespath;
|
type JmesPath = typeof jmespath;
|
||||||
|
|
||||||
export class AdvancedMatcher {
|
export interface IAdvancedMatcher {
|
||||||
|
validate<T>(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: T | undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdvancedMatcher implements IAdvancedMatcher {
|
||||||
constructor(private jmespath: JmesPath) {}
|
constructor(private jmespath: JmesPath) {}
|
||||||
|
|
||||||
private compare(actual: unknown, expected: string, method?: string): boolean {
|
private compare(actual: unknown, expected: string, method?: string): boolean {
|
||||||
@@ -12,7 +16,7 @@ export class AdvancedMatcher {
|
|||||||
return String(actual) === expected; // Default
|
return String(actual) === expected; // Default
|
||||||
}
|
}
|
||||||
|
|
||||||
validate<T>(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: any } {
|
validate<T>(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: T | undefined } {
|
||||||
const { useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor;
|
const { useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor;
|
||||||
if (!useAdvancedMatching) return { ok: true, message: "Success" };
|
if (!useAdvancedMatching) return { ok: true, message: "Success" };
|
||||||
|
|
||||||
@@ -35,10 +39,10 @@ export class AdvancedMatcher {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFalsey = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null;
|
const isFalsy = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null;
|
||||||
return {
|
return {
|
||||||
ok: !isFalsey,
|
ok: !isFalsy,
|
||||||
message: !isFalsey ? "Success" : "Extracted value is falsey",
|
message: !isFalsy ? "Success" : "Extracted value is falsy",
|
||||||
extracted: dataToValidate,
|
extracted: dataToValidate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type Got, HTTPError, RequestError } from "got";
|
import { type Got, HTTPError, RequestError } from "got";
|
||||||
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
|
import { IAdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
|
||||||
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||||
import { HttpStatusPayload } from "@/types/network.js";
|
import { HttpStatusPayload } from "@/types/network.js";
|
||||||
import { MonitorStatusResponse } from "@/types/network.js";
|
import { MonitorStatusResponse } from "@/types/network.js";
|
||||||
@@ -13,7 +13,7 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private got: Got,
|
private got: Got,
|
||||||
private advancedMatcher: AdvancedMatcher
|
private advancedMatcher: IAdvancedMatcher
|
||||||
) {
|
) {
|
||||||
const cacheable = new CacheableLookup({ maxTtl: 300, errorTtl: 30 });
|
const cacheable = new CacheableLookup({ maxTtl: 300, errorTtl: 30 });
|
||||||
this.got = got.extend({
|
this.got = got.extend({
|
||||||
|
|||||||
@@ -145,9 +145,9 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildThresholdBreachContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse<HardwareStatusPayload>): NotificationContent {
|
private buildThresholdBreachContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse<HardwareStatusPayload>): NotificationContent {
|
||||||
const title = `Threshold Breach: ${monitor.name}`;
|
const title = `Threshold Exceeded: ${monitor.name}`;
|
||||||
const summary = `Monitor "${monitor.name}" has breached one or more thresholds.`;
|
const summary = `Monitor "${monitor.name}" has exceeded one or more thresholds.`;
|
||||||
const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`];
|
const details = [`URL: ${monitor.url}`, `Status: Threshold exceeded`, `Type: ${monitor.type}`];
|
||||||
|
|
||||||
const thresholds = this.extractThresholdBreaches(monitor, monitorStatusResponse);
|
const thresholds = this.extractThresholdBreaches(monitor, monitorStatusResponse);
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export class EmailProvider implements INotificationProvider {
|
|||||||
case "monitor_up":
|
case "monitor_up":
|
||||||
return `Monitor ${message.monitor.name} is back up`;
|
return `Monitor ${message.monitor.name} is back up`;
|
||||||
case "threshold_breach":
|
case "threshold_breach":
|
||||||
return `Monitor ${message.monitor.name} threshold breached`;
|
return `Monitor ${message.monitor.name} threshold exceeded`;
|
||||||
case "threshold_resolved":
|
case "threshold_resolved":
|
||||||
return `Monitor ${message.monitor.name} thresholds resolved`;
|
return `Monitor ${message.monitor.name} thresholds resolved`;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export class StatusService implements IStatusService {
|
|||||||
|
|
||||||
// Return early if not enough data points
|
// Return early if not enough data points
|
||||||
if (monitor.statusWindow.length < monitor.statusWindowSize) {
|
if (monitor.statusWindow.length < monitor.statusWindowSize) {
|
||||||
monitor.status = newStatus;
|
monitor.status = "initializing";
|
||||||
const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor);
|
const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor);
|
||||||
return {
|
return {
|
||||||
monitor: updated,
|
monitor: updated,
|
||||||
@@ -360,8 +360,7 @@ export class StatusService implements IStatusService {
|
|||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
throw new AppError({
|
throw new AppError({
|
||||||
message: `Failed to update monitor status: ${error instanceof Error ? error.message : "Unknown error"}`,
|
message: `Failed to update monitor with id ${check.metadata.monitorId} with status: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
details: `Error updating status for monitor with id ${check.metadata.monitorId}`,
|
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "updateMonitorStatus",
|
method: "updateMonitorStatus",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ISettingsRepository } from "@/repositories/index.js";
|
import { ISettingsRepository } from "@/repositories/index.js";
|
||||||
import { Settings } from "@/types/index.js";
|
import { Settings, SettingsUpdate } 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";
|
import type { StringValue } from "ms";
|
||||||
@@ -19,6 +19,7 @@ export interface ISettingsService {
|
|||||||
loadSettings(): EnvConfig;
|
loadSettings(): EnvConfig;
|
||||||
getSettings(): EnvConfig;
|
getSettings(): EnvConfig;
|
||||||
getDBSettings(): Promise<Settings>;
|
getDBSettings(): Promise<Settings>;
|
||||||
|
updateDbSettings(newSettings: SettingsUpdate): Promise<Settings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SettingsService implements ISettingsService {
|
export class SettingsService implements ISettingsService {
|
||||||
@@ -53,7 +54,7 @@ export class SettingsService implements ISettingsService {
|
|||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDbSettings = async (newSettings: Partial<Settings>) => {
|
updateDbSettings = async (newSettings: SettingsUpdate) => {
|
||||||
return await this.settingsRepository.update(newSettings);
|
return await this.settingsRepository.update(newSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CheckSnapshot } from "@/types/check.js";
|
import type { CheckSnapshot } from "@/types/check.js";
|
||||||
export type { CheckSnapshot } from "@/types/check.js";
|
export type { CheckSnapshot } from "@/types/check.js";
|
||||||
import type { GeoContinent } from "@/types/geoCheck.js";
|
import type { GeoContinent, GroupedGeoCheck } 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", "websocket", "unknown"] as const;
|
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const;
|
||||||
@@ -74,6 +74,10 @@ export interface MonitorsWithChecksByTeamIdResult {
|
|||||||
monitors: Monitor[];
|
monitors: Monitor[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GroupedGeoCheckResult {
|
||||||
|
groupedGeoChecks: GroupedGeoCheck[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface UptimeDetailsResult {
|
export interface UptimeDetailsResult {
|
||||||
monitorData: {
|
monitorData: {
|
||||||
monitor: Monitor;
|
monitor: Monitor;
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ export interface SettingsThresholds {
|
|||||||
temperature?: number;
|
temperature?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SettingsUpdate = {
|
||||||
|
[K in keyof Settings]?: Settings[K] | null;
|
||||||
|
};
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
id: string;
|
id: string;
|
||||||
checkTTL: number;
|
checkTTL: number;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const StatusPageTypes = ["uptime"] as const;
|
export const StatusPageTypes = ["uptime", "infrastructure"] as const;
|
||||||
export type StatusPageType = (typeof StatusPageTypes)[number];
|
export type StatusPageType = (typeof StatusPageTypes)[number];
|
||||||
|
|
||||||
export interface StatusPageLogo {
|
export interface StatusPageLogo {
|
||||||
@@ -15,7 +15,7 @@ export interface StatusPage {
|
|||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
type: StatusPageType;
|
type: StatusPageType[];
|
||||||
companyName: string;
|
companyName: string;
|
||||||
url: string;
|
url: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
@@ -28,6 +28,7 @@ export interface StatusPage {
|
|||||||
showCharts: boolean;
|
showCharts: boolean;
|
||||||
showUptimePercentage: boolean;
|
showUptimePercentage: boolean;
|
||||||
showAdminLoginLink: boolean;
|
showAdminLoginLink: boolean;
|
||||||
|
showInfrastructure: boolean;
|
||||||
customCSS: string;
|
customCSS: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
export interface AppErrorConfig {
|
export interface AppErrorConfig {
|
||||||
message: string;
|
message?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
service?: string | null;
|
service?: string;
|
||||||
method?: string | null;
|
method?: string;
|
||||||
details?: unknown;
|
details?: Record<string, unknown> | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AppError extends Error {
|
export class AppError extends Error {
|
||||||
private status: number;
|
readonly status: number;
|
||||||
private service: string | null;
|
readonly service: string;
|
||||||
private method: string | null;
|
readonly method: string;
|
||||||
private details: unknown;
|
readonly details: Record<string, unknown> | undefined;
|
||||||
|
|
||||||
constructor({ message, status = 500, service = null, method = null, details = null }: AppErrorConfig) {
|
constructor({ message, status = 500, service = "unknownService", method = "unknownMethod", details = undefined }: AppErrorConfig) {
|
||||||
super(message);
|
super(message);
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.service = service;
|
this.service = service;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const NormalizeData = <T extends HasResponseTime>(checks: T[], rangeMin:
|
|||||||
const min = calculatePercentile(checks, 0);
|
const min = calculatePercentile(checks, 0);
|
||||||
const max = calculatePercentile(checks, 95);
|
const max = calculatePercentile(checks, 95);
|
||||||
const normalizedChecks = checks.map((check) => {
|
const normalizedChecks = checks.map((check) => {
|
||||||
const originalResponseTime = check.responseTime;
|
const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0;
|
||||||
let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min);
|
let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min);
|
||||||
|
|
||||||
normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime));
|
normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime));
|
||||||
@@ -59,7 +59,8 @@ export const NormalizeData = <T extends HasResponseTime>(checks: T[], rangeMin:
|
|||||||
return normalizedChecks;
|
return normalizedChecks;
|
||||||
} else {
|
} else {
|
||||||
return checks.map((check) => {
|
return checks.map((check) => {
|
||||||
return { ...check, originalResponseTime: check.responseTime };
|
const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0;
|
||||||
|
return { ...check, originalResponseTime: originalResponseTime };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -128,39 +128,38 @@ const importedMonitorSchema = z.object({
|
|||||||
teamId: z.string().optional(),
|
teamId: z.string().optional(),
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
description: z.union([z.string(), z.literal("")]).optional(),
|
description: z.union([z.string(), z.literal("")]).optional(),
|
||||||
status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).optional(),
|
status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).default("initializing"),
|
||||||
statusWindow: z.array(z.boolean()).optional(),
|
statusWindow: z.array(z.boolean()).default([]),
|
||||||
statusWindowSize: z.number().optional(),
|
statusWindowSize: z.number().min(1).max(20).default(5),
|
||||||
statusWindowThreshold: z.number().optional(),
|
statusWindowThreshold: z.number().min(1).max(100).default(60),
|
||||||
type: z.enum(MonitorTypes, "Invalid monitor type"),
|
type: z.enum(MonitorTypes, "Invalid monitor type"),
|
||||||
ignoreTlsErrors: z.boolean().optional(),
|
ignoreTlsErrors: z.boolean().default(false),
|
||||||
useAdvancedMatching: z.boolean().optional(),
|
useAdvancedMatching: z.boolean().default(false),
|
||||||
jsonPath: z.union([z.string(), z.literal("")]).optional(),
|
jsonPath: z.union([z.string(), z.literal("")]).optional(),
|
||||||
expectedValue: z.union([z.string(), z.literal("")]).optional(),
|
expectedValue: z.union([z.string(), z.literal("")]).optional(),
|
||||||
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
|
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
|
||||||
url: z.string().min(1, "URL is required"),
|
url: z.string().min(1, "URL is required"),
|
||||||
port: z.number().optional(),
|
port: z.number().optional(),
|
||||||
isActive: z.boolean().optional(),
|
isActive: z.boolean().default(true),
|
||||||
interval: z.number().optional(),
|
interval: z.number().default(60000),
|
||||||
uptimePercentage: z.number().optional(),
|
uptimePercentage: z.number().optional(),
|
||||||
notifications: z.array(z.string()).optional(),
|
notifications: z.array(z.string()).default([]),
|
||||||
secret: z.string().optional(),
|
secret: z.string().optional(),
|
||||||
cpuAlertThreshold: z.number().optional(),
|
cpuAlertThreshold: z.number().default(100),
|
||||||
cpuAlertCounter: z.number().optional(),
|
cpuAlertCounter: z.number().default(5),
|
||||||
memoryAlertThreshold: z.number().optional(),
|
memoryAlertThreshold: z.number().default(100),
|
||||||
memoryAlertCounter: z.number().optional(),
|
memoryAlertCounter: z.number().default(5),
|
||||||
diskAlertThreshold: z.number().optional(),
|
diskAlertThreshold: z.number().default(100),
|
||||||
diskAlertCounter: z.number().optional(),
|
diskAlertCounter: z.number().default(5),
|
||||||
tempAlertThreshold: z.number().optional(),
|
tempAlertThreshold: z.number().default(100),
|
||||||
tempAlertCounter: z.number().optional(),
|
tempAlertCounter: z.number().default(5),
|
||||||
selectedDisks: z.array(z.string()).optional(),
|
selectedDisks: z.array(z.string()).default([]),
|
||||||
gameId: z.union([z.string(), z.literal("")]).optional(),
|
gameId: z.union([z.string(), z.literal("")]).optional(),
|
||||||
grpcServiceName: z.union([z.string(), z.literal("")]).optional(),
|
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
|
||||||
group: z.union([z.string(), z.null()]).optional(),
|
group: z.union([z.string().max(50).trim(), z.null()]).default(null),
|
||||||
geoCheckEnabled: z.boolean().optional(),
|
geoCheckEnabled: z.boolean().default(false),
|
||||||
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
|
geoCheckLocations: z.array(z.enum(GeoContinents)).default([]),
|
||||||
geoCheckInterval: z.number().optional(),
|
geoCheckInterval: z.number().min(300000).default(300000),
|
||||||
recentChecks: z.array(z.unknown()).optional(),
|
|
||||||
createdAt: z.string().optional(),
|
createdAt: z.string().optional(),
|
||||||
updatedAt: z.string().optional(),
|
updatedAt: z.string().optional(),
|
||||||
});
|
});
|
||||||
@@ -169,6 +168,8 @@ export const importMonitorsBodyValidation = z.object({
|
|||||||
monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"),
|
monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type ImportedMonitor = z.output<typeof importedMonitorSchema>;
|
||||||
|
|
||||||
export const getHardwareDetailsByIdParamValidation = z.object({
|
export const getHardwareDetailsByIdParamValidation = z.object({
|
||||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -98,3 +98,20 @@ export const sendTestEmailBodyValidation = z.object({
|
|||||||
systemEmailRejectUnauthorized: z.boolean().optional(),
|
systemEmailRejectUnauthorized: z.boolean().optional(),
|
||||||
systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(),
|
systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const updateNotificationsValidation = z
|
||||||
|
.object({
|
||||||
|
monitorIds: z.array(z.string()).min(1, "At least one monitor ID is required").max(100, "Cannot update more than 100 monitors at once"),
|
||||||
|
notificationIds: z.array(z.string()).max(100, "Cannot specify more than 100 notification IDs at once"),
|
||||||
|
action: z.enum(["add", "remove", "set"] as const),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.action !== "set" && data.notificationIds.length === 0) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Notification IDs cannot be empty unless action is 'set'",
|
||||||
|
path: ["notificationIds"],
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -7,17 +7,17 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export const updateAppSettingsBodyValidation = z
|
export const updateAppSettingsBodyValidation = z
|
||||||
.object({
|
.object({
|
||||||
checkTTL: z.union([z.number().int().min(1).max(CHECK_TTL_SENTINEL), z.literal("")]).optional(),
|
checkTTL: z.number().int().min(1).max(CHECK_TTL_SENTINEL).optional(),
|
||||||
systemEmailPort: z.union([z.number(), z.literal("")]).optional(),
|
systemEmailPort: z.number().nullable().optional(),
|
||||||
pagespeedApiKey: z.union([z.string(), z.literal("")]).optional(),
|
pagespeedApiKey: z.string().nullable().optional(),
|
||||||
language: z.union([z.string(), z.literal("")]).optional(),
|
language: z.string().optional(),
|
||||||
timezone: z.union([z.string(), z.literal("")]).optional(),
|
timezone: z.string().optional(),
|
||||||
systemEmailHost: z.union([z.string(), z.literal("")]).optional(),
|
systemEmailHost: z.string().nullable().optional(),
|
||||||
systemEmailAddress: z.union([z.string(), z.literal("")]).optional(),
|
systemEmailAddress: z.string().nullable().optional(),
|
||||||
systemEmailPassword: z.union([z.string(), z.literal("")]).optional(),
|
systemEmailPassword: z.string().nullable().optional(),
|
||||||
systemEmailUser: z.union([z.string(), z.literal("")]).optional(),
|
systemEmailUser: z.string().nullable().optional(),
|
||||||
systemEmailConnectionHost: z.union([z.string(), z.literal("")]).optional(),
|
systemEmailConnectionHost: z.string().nullable().optional(),
|
||||||
systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(),
|
systemEmailTLSServername: z.string().nullable().optional(),
|
||||||
|
|
||||||
showURL: z.boolean().optional(),
|
showURL: z.boolean().optional(),
|
||||||
systemEmailSecure: z.boolean().optional(),
|
systemEmailSecure: z.boolean().optional(),
|
||||||
@@ -28,10 +28,10 @@ export const updateAppSettingsBodyValidation = z
|
|||||||
|
|
||||||
globalThresholds: z
|
globalThresholds: z
|
||||||
.object({
|
.object({
|
||||||
cpu: z.union([z.number().min(1).max(100), z.literal("")]).optional(),
|
cpu: z.number().min(1).max(100).optional(),
|
||||||
memory: z.union([z.number().min(1).max(100), z.literal("")]).optional(),
|
memory: z.number().min(1).max(100).optional(),
|
||||||
disk: z.union([z.number().min(1).max(100), z.literal("")]).optional(),
|
disk: z.number().min(1).max(100).optional(),
|
||||||
temperature: z.union([z.number().min(1).max(150), z.literal("")]).optional(),
|
temperature: z.number().min(1).max(150).optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { booleanCoercion } from "./shared.js";
|
import { booleanCoercion } from "./shared.js";
|
||||||
|
import { StatusPageTypes } from "@/types/statusPage.js";
|
||||||
|
|
||||||
//****************************************
|
//****************************************
|
||||||
// Status Page Validations
|
// Status Page Validations
|
||||||
@@ -10,13 +11,13 @@ export const getStatusPageParamValidation = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getStatusPageQueryValidation = z.object({
|
export const getStatusPageQueryValidation = z.object({
|
||||||
type: z.literal("uptime"),
|
type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])),
|
||||||
timeFrame: z.coerce.number().optional(),
|
timeFrame: z.coerce.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createStatusPageBodyValidation = z
|
export const createStatusPageBodyValidation = z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal("uptime"),
|
type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])),
|
||||||
companyName: z.string().min(1, "Company name is required"),
|
companyName: z.string().min(1, "Company name is required"),
|
||||||
url: z.string().regex(/^[a-zA-Z0-9_-]+$/, {
|
url: z.string().regex(/^[a-zA-Z0-9_-]+$/, {
|
||||||
message: "URL can only contain letters, numbers, underscores, and hyphens",
|
message: "URL can only contain letters, numbers, underscores, and hyphens",
|
||||||
@@ -30,6 +31,7 @@ export const createStatusPageBodyValidation = z
|
|||||||
showCharts: booleanCoercion.optional(),
|
showCharts: booleanCoercion.optional(),
|
||||||
showUptimePercentage: booleanCoercion,
|
showUptimePercentage: booleanCoercion,
|
||||||
showAdminLoginLink: booleanCoercion.optional(),
|
showAdminLoginLink: booleanCoercion.optional(),
|
||||||
|
showInfrastructure: booleanCoercion.optional(),
|
||||||
removeLogo: z.union([z.literal("true"), z.literal("false")]).optional(),
|
removeLogo: z.union([z.literal("true"), z.literal("false")]).optional(),
|
||||||
})
|
})
|
||||||
.strip();
|
.strip();
|
||||||
|
|||||||
Reference in new issue
Block a user