Compare commits

..
96 changed files with 1013 additions and 1520 deletions

No files matched your search

+22 -3
View File
@@ -83,6 +83,7 @@
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.29.0", "version": "7.29.0",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -353,6 +354,7 @@
"node_modules/@emotion/react": { "node_modules/@emotion/react": {
"version": "11.14.0", "version": "11.14.0",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.18.3", "@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5", "@emotion/babel-plugin": "^11.13.5",
@@ -390,6 +392,7 @@
"node_modules/@emotion/styled": { "node_modules/@emotion/styled": {
"version": "11.14.1", "version": "11.14.1",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.18.3", "@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5", "@emotion/babel-plugin": "^11.13.5",
@@ -1159,6 +1162,7 @@
"node_modules/@mui/material": { "node_modules/@mui/material": {
"version": "7.3.7", "version": "7.3.7",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.7", "@mui/core-downloads-tracker": "^7.3.7",
@@ -1263,6 +1267,7 @@
"node_modules/@mui/system": { "node_modules/@mui/system": {
"version": "7.3.7", "version": "7.3.7",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.4",
"@mui/private-theming": "^7.3.7", "@mui/private-theming": "^7.3.7",
@@ -2303,6 +2308,7 @@
"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"
} }
@@ -2318,6 +2324,7 @@
"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"
@@ -2439,6 +2446,7 @@
"version": "8.15.0", "version": "8.15.0",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -2763,6 +2771,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -3194,7 +3203,8 @@
}, },
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.13", "version": "1.11.13",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
@@ -3576,6 +3586,7 @@
"version": "8.57.1", "version": "8.57.1",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
@@ -4320,6 +4331,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.6" "@babel/runtime": "^7.27.6"
}, },
@@ -5103,6 +5115,7 @@
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==", "integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"peer": true,
"dependencies": { "dependencies": {
"@mapbox/geojson-rewind": "^0.5.2", "@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2",
@@ -5750,6 +5763,7 @@
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "18.3.1", "version": "18.3.1",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
@@ -5761,6 +5775,7 @@
"node_modules/react-hook-form": { "node_modules/react-hook-form": {
"version": "7.71.1", "version": "7.71.1",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -5960,7 +5975,8 @@
}, },
"node_modules/redux": { "node_modules/redux": {
"version": "5.0.1", "version": "5.0.1",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/redux-persist": { "node_modules/redux-persist": {
"version": "6.0.0", "version": "6.0.0",
@@ -6106,10 +6122,11 @@
"license": "Unlicense" "license": "Unlicense"
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.57.1", "version": "4.59.0",
"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"
}, },
@@ -6898,6 +6915,7 @@
"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"
@@ -7033,6 +7051,7 @@
"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,8 +57,7 @@ export const HeatmapResponseTimeTooltip = ({
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)} {formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
</Typography> </Typography>
<Typography> <Typography>
{t("common.labels.responseTime")}:{" "} {t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms
{check?.originalResponseTime?.toFixed() ?? "N/A"} ms
</Typography> </Typography>
<Typography textTransform={"capitalize"}> <Typography textTransform={"capitalize"}>
Status:{" "} Status:{" "}
+2 -2
View File
@@ -1,14 +1,14 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { settingsSchema } from "@/Validation/settings"; import { settingsSchema } from "@/Validation/settings";
import type { Settings } from "@/Types/Settings"; import type { Settings } from "@/Types/Settings";
import type { SettingsFormInput } from "@/Validation/settings"; import type { SettingsFormData } from "@/Validation/settings";
interface UseSettingsFormOptions { interface UseSettingsFormOptions {
data?: Settings | null; data?: Settings | null;
} }
export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => { export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => {
return useMemo(() => { return useMemo(() => {
const defaults: SettingsFormInput = { const defaults: SettingsFormData = {
systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false, systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false,
systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false, systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false,
systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true, systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true,
+24 -3
View File
@@ -20,7 +20,7 @@ import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
import { useToast } from "@/Hooks/UseToast"; import { useToast } from "@/Hooks/UseToast";
import { useSettingsForm } from "@/Hooks/useSettingsForm"; import { useSettingsForm } from "@/Hooks/useSettingsForm";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import type { SettingsFormData, SettingsFormInput } from "@/Validation/settings"; import type { SettingsFormData } from "@/Validation/settings";
import { useState } from "react"; import { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs"; import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs";
@@ -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<SettingsFormInput, unknown, SettingsFormData>({ const form = useForm<SettingsFormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: defaults, defaultValues: defaults,
mode: "onChange", mode: "onChange",
@@ -256,7 +256,28 @@ export const SettingsPage = () => {
delete (dataToSend as any).systemEmailPassword; delete (dataToSend as any).systemEmailPassword;
} }
const result = await patch("/settings", dataToSend as SettingsFormData); // Convert undefined to empty string for backend unsetting
const processedData = Object.entries(dataToSend).reduce((acc, [key, value]) => {
const typedKey = key as keyof SettingsFormData;
if (value === undefined) {
(acc as any)[typedKey] = "";
} else if (typeof value === "object" && value !== null) {
// Handle nested objects like globalThresholds
// Convert 0 to "" for unsetting thresholds
(acc as any)[typedKey] = Object.entries(value).reduce(
(nested, [nestedKey, nestedValue]) => ({
...nested,
[nestedKey]: nestedValue === undefined ? "" : nestedValue,
}),
{}
);
} else {
(acc as any)[typedKey] = value;
}
return acc;
}, {} as Partial<SettingsFormData>);
const result = await patch("/settings", processedData as SettingsFormData);
if (result?.success) { if (result?.success) {
// Update API key state from response // Update API key state from response
+20 -11
View File
@@ -29,25 +29,32 @@ import type { StatusPageFormData } from "@/Validation/statusPage";
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi"; import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
import type { Monitor } from "@/Types/Monitor"; import type { Monitor } from "@/Types/Monitor";
import type { MonitorDisplayType, StatusPageResponse } from "@/Types/StatusPage"; import type { 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;
} }
const MONITOR_TYPE_KEYS: Record<string, 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",
};
const getMonitorTypeLabel = (type: string, t: (key: string) => string): string => {
const i18nMonitorKey = MONITOR_TYPE_KEYS[type];
return i18nMonitorKey ? t(i18nMonitorKey) : type;
};
const CreateStatusPage = () => { const CreateStatusPage = () => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -62,7 +69,9 @@ const CreateStatusPage = () => {
isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure` isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure`
); );
const { data: monitorsResponse } = useGet<Monitor[]>(monitorsUrl); const { data: monitorsResponse } = useGet<Monitor[]>(
"/monitors/team?type=http&type=ping&type=port&type=docker&type=hardware"
);
const monitors = monitorsResponse ?? []; const monitors = monitorsResponse ?? [];
const { post, loading: isSubmittingPost } = usePost(); const { post, loading: isSubmittingPost } = usePost();
@@ -102,7 +111,7 @@ const CreateStatusPage = () => {
}); });
return typesSet.size ? Array.from(typesSet) : ["uptime"]; return typesSet.size ? Array.from(typesSet) : ["uptime"];
}, [watchedMonitorIds, monitors]); }, [JSON.stringify(watchedMonitorIds), monitors]);
useEffect(() => { useEffect(() => {
form.setValue("type", computedTypes); form.setValue("type", computedTypes);
@@ -12,7 +12,6 @@ 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"; import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
@@ -26,98 +25,81 @@ interface MonitorsListProps {
} }
const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => { const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => {
const bg = // for future: specific badge styles can be defined here based on monitor type
monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light; const badgeConfig: Record<string, { color: string; bg: string }> = {
hardware: {
color: theme.palette.background.paper,
bg: theme.palette.info.light,
},
http: {
color: theme.palette.background.paper,
bg: theme.palette.success.light,
},
ping: {
color: theme.palette.background.paper,
bg: theme.palette.success.light,
},
port: {
color: theme.palette.background.paper,
bg: theme.palette.success.light,
},
docker: {
color: theme.palette.background.paper,
bg: theme.palette.success.light,
},
pagespeed: {
color: theme.palette.background.paper,
bg: theme.palette.success.light,
},
game: {
color: theme.palette.background.paper,
bg: theme.palette.success.light,
},
};
const config = badgeConfig[monitorType] ?? badgeConfig.http;
return { return {
backgroundColor: bg, backgroundColor: config.bg,
color: theme.palette.background.paper, color: config.color,
padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`, padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`,
borderRadius: theme.shape.borderRadius, borderRadius: theme.shape.borderRadius,
}; };
}; };
const MonitorHeader = ({ // Extracted component for monitor content below the header row
monitor,
showURL,
}: {
monitor: StatusPageMonitor;
showURL: boolean;
}) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
gap={theme.spacing(LAYOUT.XS)}
mb={theme.spacing(LAYOUT.XS)}
>
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(SPACING.LG)}
mb={theme.spacing(SPACING.SM)}
>
<Typography
variant="h6"
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
>
{monitor.name}
</Typography>
<Typography
variant="caption"
sx={getMonitorBadgeStyles(monitor.type ?? "", theme)}
>
{getMonitorTypeLabel(monitor.type, t)}
</Typography>
</Stack>
{showURL && monitor.url && (
<Typography
variant="body2"
color={theme.palette.text.secondary}
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
>
{monitor.url}
</Typography>
)}
</Box>
<StatusLabel status={monitor.status} />
</Stack>
);
};
const MonitorContent = ({ const MonitorContent = ({
monitor, monitor,
statusPage, statusPage,
chartType, chartType,
}: { }: {
monitor: StatusPageMonitor; monitor: Monitor & { checks?: Monitor["recentChecks"] };
statusPage: StatusPage; statusPage: StatusPage;
chartType: string; chartType: string;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const isInfrastructureMonitor = monitor?.type === "hardware";
if (monitor.type === "hardware") { if (isInfrastructureMonitor) {
if (statusPage.showInfrastructure === false) return null; if (statusPage.showInfrastructure === false) {
return null;
}
return <InfrastructureMetrics monitor={monitor} />; return <InfrastructureMetrics monitor={monitor} />;
} }
if (statusPage.showCharts === false) return null; if (statusPage.showCharts === false) {
return null;
}
const checks = monitor.checks?.slice().reverse() ?? [];
return ( return (
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1, mb: theme.spacing(SPACING.LG) }}> <Box sx={{ overflow: "hidden", minWidth: 0, flex: 1, mb: theme.spacing(SPACING.LG) }}>
{chartType === "histogram" ? ( {chartType === "histogram" ? (
<HistogramResponseTime <HistogramResponseTime
height={{ xs: 50, md: 100 }} height={{ xs: 50, md: 100 }}
gap={{ xs: theme.spacing(SPACING.SM), md: theme.spacing(LAYOUT.SM) }} gap={{ xs: theme.spacing(SPACING.SM), md: theme.spacing(LAYOUT.SM) }}
checks={checks} checks={monitor?.checks?.slice().reverse() ?? []}
/> />
) : ( ) : (
<HeatmapResponseTime checks={checks} /> <HeatmapResponseTime checks={monitor?.checks?.slice().reverse() ?? []} />
)} )}
</Box> </Box>
); );
@@ -126,15 +108,17 @@ const MonitorContent = ({
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const showURL = useSelector((state: RootState) => state.ui?.showURL); const showURL = useSelector((state: RootState) => state.ui?.showURL);
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram"); const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
return ( return (
<Stack gap={theme.spacing(LAYOUT.MD)}> <Stack gap={theme.spacing(LAYOUT.MD)}>
{statusPage.showCharts && ( {statusPage.showCharts && (
<Stack <Stack
direction="row" direction={"row"}
alignItems="center" alignItems={"center"}
gap={theme.spacing(LAYOUT.SM)} gap={theme.spacing(LAYOUT.SM)}
> >
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography> <Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
@@ -142,7 +126,9 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
dualOption dualOption
value={chartType} value={chartType}
checked={chartType === "histogram"} checked={chartType === "histogram"}
onChange={(e) => setChartType(e.target.checked ? "histogram" : "heatmap")} onChange={(e) => {
setChartType(e.target.checked ? "histogram" : "heatmap");
}}
/> />
<Typography> <Typography>
{t("pages.statusPages.monitorsList.chartTypeHistogram")} {t("pages.statusPages.monitorsList.chartTypeHistogram")}
@@ -150,22 +136,69 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
</Stack> </Stack>
)} )}
{monitors.map((monitor) => ( {monitors?.map((monitor) => {
<BaseBox return (
key={monitor.id} <BaseBox
padding={theme.spacing(LAYOUT.MD)} key={monitor.id}
> padding={theme.spacing(LAYOUT.MD)}
<MonitorHeader >
monitor={monitor} <Stack
showURL={showURL} direction="row"
/> alignItems="center"
<MonitorContent justifyContent="space-between"
monitor={monitor} gap={theme.spacing(LAYOUT.XS)}
statusPage={statusPage} mb={theme.spacing(LAYOUT.XS)}
chartType={chartType} >
/> <Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
</BaseBox> <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)}
>
{t(
`pages.common.monitors.monitorTypes.option${monitor?.type.charAt(0).toUpperCase() + monitor?.type.slice(1)}`
)}
</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>
<MonitorContent
monitor={monitor}
statusPage={statusPage}
chartType={chartType}
/>
</BaseBox>
);
})}
</Stack> </Stack>
); );
}; };
+3 -8
View File
@@ -25,20 +25,15 @@ const StatusPageView = () => {
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null; const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
const { data, isLoading, error } = useGet<StatusPageResponse>( const { data, isLoading, error } = useGet<StatusPageResponse>(apiUrl);
apiUrl,
{},
{
refreshInterval: 10000,
}
);
const statusPage = data?.statusPage; const statusPage = data?.statusPage;
const monitors = data?.monitors ?? []; const monitors = data?.monitors ?? [];
const infrastructureMonitors = data?.infrastructureMonitors ?? [];
if (!statusPage) return null; if (!statusPage) return null;
if (monitors.length === 0) { if (monitors?.length === 0 && infrastructureMonitors?.length === 0) {
return ( return (
<BasePage <BasePage
loading={isLoading} loading={isLoading}
+2 -21
View File
@@ -1,26 +1,6 @@
import type { Monitor, MonitorType } from "@/Types/Monitor"; import type { Monitor } from "@/Types/Monitor";
export type MonitorDisplayType = "uptime" | "infrastructure"; 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;
@@ -50,4 +30,5 @@ export interface StatusPage {
export interface StatusPageResponse { export interface StatusPageResponse {
statusPage: StatusPage; statusPage: StatusPage;
monitors: Monitor[]; monitors: Monitor[];
infrastructureMonitors?: Monitor[];
} }
+8 -8
View File
@@ -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() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
systemEmailSecure: z.boolean().optional(), systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(), systemEmailPool: z.boolean().optional(),
@@ -19,30 +19,31 @@ 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() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
systemEmailHost: z systemEmailHost: z
.string() .string()
.regex(/^[a-zA-Z0-9.-]*$/, "Invalid hostname or IP address") .regex(/^[a-zA-Z0-9.-]*$/, "Invalid hostname or IP address")
.transform((val) => (val.trim() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
systemEmailPort: z.number().int().min(1).max(65535).optional(), systemEmailPort: z.number().int().min(1).max(65535).optional(),
systemEmailAddress: z systemEmailAddress: z
.string()
.email("Please enter a valid email address") .email("Please enter a valid email address")
.or(z.literal("")) .or(z.literal(""))
.transform((val) => (val === "" ? null : val.toLowerCase().trim())) .transform((val) => (val === "" ? undefined : val.toLowerCase().trim()))
.optional(), .optional(),
systemEmailUser: z systemEmailUser: z
.string() .string()
.transform((val) => (val.trim() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
systemEmailPassword: z systemEmailPassword: z
.string() .string()
.transform((val) => (val.trim() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
systemEmailTLSServername: z systemEmailTLSServername: z
.string() .string()
.transform((val) => (val.trim() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
globalThresholds: z.object({ globalThresholds: z.object({
cpu: z.number().int().min(1).max(100), cpu: z.number().int().min(1).max(100),
@@ -52,5 +53,4 @@ export const settingsSchema = z.object({
}), }),
}); });
export type SettingsFormInput = z.input<typeof settingsSchema>;
export type SettingsFormData = z.infer<typeof settingsSchema>; export type SettingsFormData = z.infer<typeof settingsSchema>;
+1 -1
View File
@@ -401,7 +401,7 @@
}, },
"status": { "status": {
"down": "down", "down": "down",
"breached": "threshold exceeded", "breached": "breached",
"initializing": "initializing", "initializing": "initializing",
"maintenance": "maintenance", "maintenance": "maintenance",
"paused": "paused", "paused": "paused",
+1 -4
View File
@@ -32,14 +32,11 @@ export default [
languageOptions: { languageOptions: {
parser: tseslint.parser, parser: tseslint.parser,
parserOptions: { parserOptions: {
projectService: { projectService: true,
allowDefaultProject: ["jest.config.ts"],
},
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
}, },
rules: { rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], // Ignore unused variables that start with '_'
"@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types "@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing "mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
}, },
+135 -172
View File
@@ -510,24 +510,42 @@
"summary": "Get monitors by team", "summary": "Get monitors by team",
"description": "Get monitors filtered by team with optional parameters", "description": "Get monitors filtered by team with optional parameters",
"parameters": [ "parameters": [
{
"name": "status",
"in": "query",
"description": "Filter by monitor status (up/down)",
"schema": {
"type": "boolean"
}
},
{ {
"name": "type", "name": "type",
"in": "query", "in": "query",
"description": "Filter by monitor type", "description": "Filter by monitor type",
"schema": { "schema": {
"oneOf": [ "type": "string",
{ "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"]
"type": "string", }
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"] },
}, {
{ "name": "page",
"type": "array", "in": "query",
"items": { "description": "Page number for pagination",
"type": "string", "schema": {
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"] "type": "integer",
} "minimum": 1,
} "default": 1
] }
},
{
"name": "rowsPerPage",
"in": "query",
"description": "Number of monitors per page",
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 10
} }
}, },
{ {
@@ -537,6 +555,15 @@
"schema": { "schema": {
"type": "string" "type": "string"
} }
},
{
"name": "field",
"in": "query",
"description": "Field to filter on",
"schema": {
"type": "string",
"enum": ["name", "url", "description"]
}
} }
], ],
"responses": { "responses": {
@@ -729,7 +756,7 @@
} }
] ]
}, },
"patch": { "put": {
"tags": ["monitors"], "tags": ["monitors"],
"summary": "Update monitor", "summary": "Update monitor",
"description": "Update an existing monitor", "description": "Update an existing monitor",
@@ -3541,19 +3568,22 @@
}, },
"CreateMonitorRequest": { "CreateMonitorRequest": {
"type": "object", "type": "object",
"required": ["name", "type", "url"], "required": ["name", "description", "type", "url", "interval"],
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
"minLength": 1,
"maxLength": 100,
"example": "My Website Monitor" "example": "My Website Monitor"
}, },
"description": { "description": {
"type": "string", "type": "string",
"maxLength": 500,
"example": "Monitors the main website homepage" "example": "Monitors the main website homepage"
}, },
"type": { "type": {
"type": "string", "type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"], "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"],
"example": "http" "example": "http"
}, },
"url": { "url": {
@@ -3563,34 +3593,14 @@
}, },
"interval": { "interval": {
"type": "integer", "type": "integer",
"minimum": 30,
"maximum": 3600,
"example": 300, "example": 300,
"description": "Check interval in seconds" "description": "Check interval in seconds"
}, },
"statusWindowSize": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"default": 5
},
"statusWindowThreshold": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 60
},
"isActive": { "isActive": {
"type": "boolean"
},
"ignoreTlsErrors": {
"type": "boolean", "type": "boolean",
"default": false "default": true
},
"useAdvancedMatching": {
"type": "boolean",
"default": false
},
"port": {
"type": "integer"
}, },
"notifications": { "notifications": {
"type": "array", "type": "array",
@@ -3599,62 +3609,51 @@
}, },
"description": "Array of notification IDs to associate with this monitor" "description": "Array of notification IDs to associate with this monitor"
}, },
"secret": { "httpOptions": {
"type": "string" "type": "object",
}, "properties": {
"jsonPath": { "method": {
"type": "string" "type": "string",
}, "enum": ["GET", "POST", "PUT", "DELETE", "HEAD"],
"expectedValue": { "default": "GET"
"type": "string" },
}, "headers": {
"matchMethod": { "type": "object",
"type": "string", "additionalProperties": {
"enum": ["equal", "include", "regex", ""] "type": "string"
}, }
"cpuAlertThreshold": { },
"type": "number" "body": {
}, "type": "string",
"memoryAlertThreshold": { "description": "Request body for POST/PUT requests"
"type": "number" },
}, "timeout": {
"diskAlertThreshold": { "type": "integer",
"type": "number" "minimum": 1000,
}, "maximum": 30000,
"tempAlertThreshold": { "default": 5000,
"type": "number" "description": "Request timeout in milliseconds"
}, }
"gameId": {
"type": "string"
},
"grpcServiceName": {
"type": "string",
"default": ""
},
"selectedDisks": {
"type": "array",
"items": {
"type": "string"
} }
}, },
"group": { "assertions": {
"type": "string",
"nullable": true,
"maxLength": 50
},
"geoCheckEnabled": {
"type": "boolean"
},
"geoCheckLocations": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "object",
"enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"] "properties": {
"type": {
"type": "string",
"enum": ["status-code", "response-time", "body-contains", "header-contains"]
},
"comparison": {
"type": "string",
"enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"]
},
"value": {
"type": "string"
}
}
} }
},
"geoCheckInterval": {
"type": "integer",
"minimum": 300000
} }
} }
}, },
@@ -3662,32 +3661,21 @@
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "name": {
"type": "string"
},
"type": {
"type": "string", "type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] "minLength": 1,
}, "maxLength": 100
"url": {
"type": "string"
},
"statusWindowSize": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"default": 5
},
"statusWindowThreshold": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 60
}, },
"description": { "description": {
"type": "string" "type": "string",
"maxLength": 500
}, },
"interval": { "interval": {
"type": "integer" "type": "integer",
"minimum": 30,
"maximum": 3600
},
"isActive": {
"type": "boolean"
}, },
"notifications": { "notifications": {
"type": "array", "type": "array",
@@ -3695,72 +3683,47 @@
"type": "string" "type": "string"
} }
}, },
"secret": { "httpOptions": {
"type": "string" "type": "object",
}, "properties": {
"ignoreTlsErrors": { "method": {
"type": "boolean" "type": "string",
}, "enum": ["GET", "POST", "PUT", "DELETE", "HEAD"]
"useAdvancedMatching": { },
"type": "boolean" "headers": {
}, "type": "object",
"jsonPath": { "additionalProperties": {
"type": "string" "type": "string"
}, }
"expectedValue": { },
"type": "string" "body": {
}, "type": "string"
"matchMethod": { },
"type": "string", "timeout": {
"enum": ["equal", "include", "regex", ""] "type": "integer",
}, "minimum": 1000,
"port": { "maximum": 30000
"type": "integer", }
"minimum": 1,
"maximum": 65535
},
"cpuAlertThreshold": {
"type": "number"
},
"memoryAlertThreshold": {
"type": "number"
},
"diskAlertThreshold": {
"type": "number"
},
"tempAlertThreshold": {
"type": "number"
},
"gameId": {
"type": "string"
},
"grpcServiceName": {
"type": "string"
},
"selectedDisks": {
"type": "array",
"items": {
"type": "string"
} }
}, },
"group": { "assertions": {
"type": "string",
"nullable": true,
"maxLength": 50
},
"geoCheckEnabled": {
"type": "boolean"
},
"geoCheckLocations": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "object",
"enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"] "properties": {
"type": {
"type": "string",
"enum": ["status-code", "response-time", "body-contains", "header-contains"]
},
"comparison": {
"type": "string",
"enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"]
},
"value": {
"type": "string"
}
}
} }
},
"geoCheckInterval": {
"type": "integer",
"minimum": 300000
} }
} }
}, },
+1 -1
View File
@@ -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 && cp -r src/templates dist/templates", "build": "tsc && tsc-alias",
"lint": "eslint .", "lint": "eslint .",
"lint-fix": "eslint --fix .", "lint-fix": "eslint --fix .",
"format": "prettier --write .", "format": "prettier --write .",
+2 -2
View File
@@ -4,7 +4,7 @@ import cors from "cors";
import helmet from "helmet"; import helmet from "helmet";
import compression from "compression"; import compression from "compression";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import swaggerUi, { type JsonObject } from "swagger-ui-express"; import swaggerUi from "swagger-ui-express";
import { handleErrors } from "@/middleware/handleErrors.js"; import { handleErrors } from "@/middleware/handleErrors.js";
import { generalApiLimiter } from "@/middleware/rateLimiter.js"; import { generalApiLimiter } from "@/middleware/rateLimiter.js";
import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js"; import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js";
@@ -24,7 +24,7 @@ export const createApp = ({
controllers: InitializedControllers; controllers: InitializedControllers;
envSettings: EnvConfig; envSettings: EnvConfig;
frontendPath: string; frontendPath: string;
openApiSpec: JsonObject; openApiSpec: any;
}) => { }) => {
const allowedOrigin = envSettings.clientHost; const allowedOrigin = envSettings.clientHost;
const app = express(); const app = express();
+1 -1
View File
@@ -31,7 +31,7 @@ export interface InitializedControllers {
export const initializeControllers = (services: InitializedServices): InitializedControllers => { export const initializeControllers = (services: InitializedServices): InitializedControllers => {
return { return {
authController: new AuthController(services.userService), authController: new AuthController(services.userService),
monitorController: new MonitorController(services.monitorService, services.notificationsService), monitorController: new MonitorController(services.monitorService),
settingsController: new SettingsController(services.settingsService, services.emailService), settingsController: new SettingsController(services.settingsService, services.emailService),
checkController: new CheckController(services.checkService), checkController: new CheckController(services.checkService),
geoCheckController: new GeoCheckController(services.geoChecksService), geoCheckController: new GeoCheckController(services.geoChecksService),
+1 -4
View File
@@ -1,9 +1,6 @@
import type { Application } from "express";
import { createVerifyJWT } from "../middleware/verifyJWT.js"; import { createVerifyJWT } from "../middleware/verifyJWT.js";
import { createVerifyStatusPageAccess } from "../middleware/verifyStatusPageAccess.js"; import { createVerifyStatusPageAccess } from "../middleware/verifyStatusPageAccess.js";
import { authApiLimiter } from "../middleware/rateLimiter.js"; import { authApiLimiter } from "../middleware/rateLimiter.js";
import type { InitializedControllers } from "./controllers.js";
import type { InitializedServices } from "./services.js";
import AuthRoutes from "../routes/authRoute.js"; import AuthRoutes from "../routes/authRoute.js";
import InviteRoutes from "../routes/inviteRoute.js"; import InviteRoutes from "../routes/inviteRoute.js";
@@ -20,7 +17,7 @@ import NotificationRoutes from "../routes/notificationRoute.js";
import IncidentRoutes from "../routes/incidentRoute.js"; import IncidentRoutes from "../routes/incidentRoute.js";
export const setupRoutes = (app: Application, controllers: InitializedControllers, services: InitializedServices) => { export const setupRoutes = (app: any, controllers: Record<string, any>, services: Record<string, any>) => {
const verifyJWT = createVerifyJWT(services.settingsService); const verifyJWT = createVerifyJWT(services.settingsService);
const authRoutes = new AuthRoutes(controllers.authController, verifyJWT); const authRoutes = new AuthRoutes(controllers.authController, verifyJWT);
const monitorRoutes = new MonitorRoutes(controllers.monitorController); const monitorRoutes = new MonitorRoutes(controllers.monitorController);
+1 -18
View File
@@ -23,24 +23,7 @@ import { IUserService } from "@/service/index.js";
const SERVICE_NAME = "authController"; const SERVICE_NAME = "authController";
export interface IAuthController { class AuthController {
registerUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
createUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
loginUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
editUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
checkSuperadminExists(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
requestRecovery(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
validateRecovery(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
resetPassword(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
deleteUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
deleteUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
getAllUsers(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
getUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
editUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
editUserPasswordById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
}
class AuthController implements IAuthController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private userService: IUserService; private userService: IUserService;
+1 -10
View File
@@ -12,16 +12,7 @@ import { requireTeamId } from "@/controllers/controllerUtils.js";
const SERVICE_NAME = "checkController"; const SERVICE_NAME = "checkController";
export interface ICheckController { class CheckController {
serviceName: string;
getChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getChecksByTeam: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getChecksSummaryByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
deleteChecks: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
deleteChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class CheckController implements ICheckController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private checkService: ICheckService; private checkService: ICheckService;
+23 -17
View File
@@ -3,7 +3,7 @@ import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index
import sslChecker, { SSLDetails } from "ssl-checker"; import sslChecker, { SSLDetails } from "ssl-checker";
type SSLCheckerType = typeof sslChecker; type SSLCheckerType = typeof sslChecker;
export const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise<SSLDetails> => { const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise<SSLDetails> => {
const monitorUrl = new URL(monitor.url); const monitorUrl = new URL(monitor.url);
const hostname = monitorUrl.hostname; const hostname = monitorUrl.hostname;
const cert = await checker(hostname); const cert = await checker(hostname);
@@ -12,14 +12,14 @@ export const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor:
} }
return cert; return cert;
}; };
export const requireString = (value: unknown, fieldName: string): string => { const requireString = (value: unknown, fieldName: string): string => {
if (typeof value === "string" && value.trim().length > 0) { if (typeof value === "string" && value.trim().length > 0) {
return value; return value;
} }
throw new AppError({ message: `${fieldName} is required`, status: 400 }); throw new AppError({ message: `${fieldName} is required`, status: 400 });
}; };
export const optionalString = (value: unknown, fieldName: string): string | undefined => { const optionalString = (value: unknown, fieldName: string): string | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -29,7 +29,7 @@ export const optionalString = (value: unknown, fieldName: string): string | unde
throw new AppError({ message: `${fieldName} must be a string`, status: 400 }); throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
}; };
export const optionalNumber = (value: unknown, fieldName: string): number | undefined => { const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -45,7 +45,7 @@ export const optionalNumber = (value: unknown, fieldName: string): number | unde
throw new AppError({ message: `${fieldName} must be a number`, status: 400 }); throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
}; };
export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => { const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -63,7 +63,7 @@ export const optionalBoolean = (value: unknown, fieldName: string): boolean | un
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 }); throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
}; };
export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => { const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
const parseSingle = (input: unknown): MonitorType => { const parseSingle = (input: unknown): MonitorType => {
if (typeof input !== "string") { if (typeof input !== "string") {
throw new AppError({ message: "Monitor type must be a string", status: 400 }); throw new AppError({ message: "Monitor type must be a string", status: 400 });
@@ -83,7 +83,7 @@ export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorTyp
return parseSingle(value); return parseSingle(value);
}; };
export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => { const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -93,36 +93,42 @@ export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 }); throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 });
}; };
export const requireTeamId = (teamId?: string): string => { const requireTeamId = (teamId?: string): string => {
if (!teamId) { if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 }); throw new AppError({ message: "Team ID is required", status: 400 });
} }
return teamId; return teamId;
}; };
export const requireUserId = (userId?: string): string => { const requireUserId = (userId?: string): string => {
if (!userId) { if (!userId) {
throw new AppError({ message: "User ID is required", status: 400 }); throw new AppError({ message: "User ID is required", status: 400 });
} }
return userId; return userId;
}; };
export const requireUserEmail = (userEmail?: string): string => { const requireUserEmail = (userEmail?: string): string => {
if (!userEmail) { if (!userEmail) {
throw new AppError({ message: "User email is required", status: 400 }); throw new AppError({ message: "User email is required", status: 400 });
} }
return userEmail; return userEmail;
}; };
export const requireFirstName = (firstName?: string): string => {
if (!firstName) {
throw new AppError({ message: "First name is required", status: 400 });
}
return firstName;
};
export const requireUserRoles = (userRoles?: UserRole[]): UserRole[] => { export const requireUserRoles = (userRoles?: UserRole[]): UserRole[] => {
if (!userRoles || userRoles.length === 0) { if (!userRoles || userRoles.length === 0) {
throw new AppError({ message: "User roles are required", status: 400 }); throw new AppError({ message: "User roles are required", status: 400 });
} }
return userRoles; return userRoles;
}; };
export {
fetchMonitorCertificate,
requireString,
optionalString,
optionalNumber,
optionalBoolean,
parseMonitorTypeFilter,
parseSortOrder,
requireTeamId,
requireUserId,
requireUserEmail,
};
@@ -2,12 +2,7 @@ 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;
+6 -27
View File
@@ -1,15 +1,10 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js"; import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
import type { IGeoChecksService } from "@/service/business/geoChecksService.js"; import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
import { requireTeamId } from "./controllerUtils.js";
import { AppError } from "@/utils/AppError.js";
const SERVICE_NAME = "geoCheckController"; const SERVICE_NAME = "geoCheckController";
export interface IGeoCheckController { class GeoCheckController {
getGeoChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class GeoCheckController implements IGeoCheckController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private geoChecksService: IGeoChecksService; private geoChecksService: IGeoChecksService;
@@ -23,29 +18,13 @@ class GeoCheckController implements IGeoCheckController {
getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getChecksParamValidation.parse(req.params); getChecksParamValidation.parse(req.params);
const validatedQuery = getChecksQueryValidation.parse(req.query); getChecksQueryValidation.parse(req.query);
if (!validatedQuery.continent || validatedQuery.continent.length === 0) {
throw new AppError({
message: "At least one continent must be specified",
service: SERVICE_NAME,
method: "getGeoChecksByMonitor",
details: { monitorId: validatedParams.monitorId },
status: 400,
});
}
const teamId = requireTeamId(req.user?.teamId);
const result = await this.geoChecksService.getGeoChecksByMonitor({ const result = await this.geoChecksService.getGeoChecksByMonitor({
monitorId: validatedParams.monitorId, monitorId: req?.params?.monitorId as string,
teamId: teamId, query: req?.query,
sortOrder: validatedQuery.sortOrder, teamId: req?.user?.teamId as string,
dateRange: validatedQuery.dateRange,
page: validatedQuery.page,
rowsPerPage: validatedQuery.rowsPerPage,
continent: validatedQuery.continent,
}); });
return res.status(200).json({ return res.status(200).json({
+21 -22
View File
@@ -1,37 +1,35 @@
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js"; import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js";
import { IIncidentService } from "@/service/index.js";
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
const SERVICE_NAME = "IncidentController"; const SERVICE_NAME = "incidentController";
export interface IIncidentController { class IncidentController {
getIncidentsByTeam: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; static SERVICE_NAME = SERVICE_NAME;
getIncidentSummary: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getIncidentById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; private incidentService: any;
resolveIncidentManually: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; constructor(incidentService: any) {
}
class IncidentController implements IIncidentController {
private incidentService: IIncidentService;
constructor(incidentService: IIncidentService) {
this.incidentService = incidentService; this.incidentService = incidentService;
} }
get serviceName() {
return IncidentController.SERVICE_NAME;
}
getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => { getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getIncidentsByTeamQueryValidation.parse(req.query); const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {};
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const result = await this.incidentService.getIncidentsByTeam( const result = await this.incidentService.getIncidentsByTeam(
teamId, teamId,
validatedQuery.sortOrder, sortOrder,
validatedQuery.dateRange, dateRange,
validatedQuery.page, page,
validatedQuery.rowsPerPage, rowsPerPage,
validatedQuery.status, status,
validatedQuery.monitorId, monitorId,
validatedQuery.resolutionType resolutionType
); );
return res.status(200).json({ return res.status(200).json({
@@ -47,9 +45,10 @@ class IncidentController implements IIncidentController {
getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => { getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const validatedQuery = getIncidentSummaryQueryValidation.parse(req.query);
const summary = await this.incidentService.getIncidentSummary(teamId, validatedQuery.limit); const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10;
const summary = await this.incidentService.getIncidentSummary(teamId, limit);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Incident summary retrieved successfully", msg: "Incident summary retrieved successfully",
+7 -15
View File
@@ -1,19 +1,12 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js"; import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js";
import { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js"; import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
import { IInviteService } from "@/service/index.js";
const SERVICE_NAME = "inviteController"; const SERVICE_NAME = "inviteController";
export interface IInviteController { class InviteController {
getInviteToken: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
sendInviteEmail: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
verifyInviteToken: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class InviteController implements IInviteController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private inviteService: IInviteService; private inviteService: any;
constructor(inviteService: IInviteService) { constructor(inviteService: any) {
this.inviteService = inviteService; this.inviteService = inviteService;
} }
@@ -41,9 +34,8 @@ class InviteController implements IInviteController {
sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => { sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const userRoles = requireUserRoles(req.user?.role); const userRoles = requireUserRoles(req?.user?.role);
const firstName = requireFirstName(req.user?.firstName);
const inviteRequest = req.body; const inviteRequest = req.body;
inviteRequest.teamId = teamId; inviteRequest.teamId = teamId;
@@ -51,7 +43,7 @@ class InviteController implements IInviteController {
const inviteToken = await this.inviteService.sendInviteEmail({ const inviteToken = await this.inviteService.sendInviteEmail({
invite: inviteRequest, invite: inviteRequest,
firstName, firstName: req?.user?.firstName,
userRoles, userRoles,
}); });
return res.status(200).json({ return res.status(200).json({
+3 -8
View File
@@ -1,16 +1,11 @@
import { ILogger } from "@/utils/logger.js";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
const SERVICE_NAME = "LogController"; const SERVICE_NAME = "LogController";
export interface ILogController {
getLogs: (req: Request, res: Response, next: NextFunction) => Promise<void>;
}
class LogController { class LogController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger; private logger: any;
constructor(logger: ILogger) { constructor(logger: any) {
this.logger = logger; this.logger = logger;
} }
get serviceName() { get serviceName() {
@@ -19,7 +14,7 @@ class LogController {
getLogs = async (req: Request, res: Response, next: NextFunction) => { getLogs = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const logs = this.logger.getLogs(); const logs = await this.logger.getLogs();
res.status(200).json({ res.status(200).json({
success: true, success: true,
msg: "Logs fetched successfully", msg: "Logs fetched successfully",
@@ -9,37 +9,26 @@ import {
deleteMaintenanceWindowByIdParamValidation, deleteMaintenanceWindowByIdParamValidation,
} from "@/validation/maintenanceWindowValidation.js"; } from "@/validation/maintenanceWindowValidation.js";
import { requireTeamId } from "@/controllers/controllerUtils.js"; import { requireTeamId } from "@/controllers/controllerUtils.js";
import { IMaintenanceWindowService } from "@/service/index.js";
export interface IMaintenanceWindowController { const SERVICE_NAME = "maintenanceWindowController";
createMaintenanceWindows: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getMaintenanceWindowById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; class MaintenanceWindowController {
getMaintenanceWindowsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; static SERVICE_NAME = SERVICE_NAME;
getMaintenanceWindowsByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; private maintenanceWindowService: any;
deleteMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; constructor(maintenanceWindowService: any) {
editMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class MaintenanceWindowController implements IMaintenanceWindowController {
private maintenanceWindowService: IMaintenanceWindowService;
constructor(maintenanceWindowService: IMaintenanceWindowService) {
this.maintenanceWindowService = maintenanceWindowService; this.maintenanceWindowService = maintenanceWindowService;
} }
get serviceName() {
return MaintenanceWindowController.SERVICE_NAME;
}
createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => { createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body); createMaintenanceWindowBodyValidation.parse(req.body);
const teamId = requireTeamId(req?.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitorIDs = validatedBody.monitors; await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
const name = validatedBody.name;
const active = validatedBody.active ?? true;
const duration = validatedBody.duration;
const durationUnit = validatedBody.durationUnit;
const repeat = validatedBody.repeat;
const start = validatedBody.start;
const end = validatedBody.end;
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, monitorIDs, name, active, duration, durationUnit, repeat, start, end });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -51,11 +40,11 @@ class MaintenanceWindowController implements IMaintenanceWindowController {
}; };
getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => { getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMaintenanceWindowByIdParamValidation.parse(req.params); getMaintenanceWindowByIdParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: validatedParams.id, teamId }); const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id as string, teamId });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -69,18 +58,11 @@ class MaintenanceWindowController implements IMaintenanceWindowController {
getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query); getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query);
const teamId = requireTeamId(req?.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query });
teamId,
active: validatedQuery.active,
page: validatedQuery.page,
rowsPerPage: validatedQuery.rowsPerPage,
field: validatedQuery.field,
order: validatedQuery.order,
});
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -94,12 +76,12 @@ class MaintenanceWindowController implements IMaintenanceWindowController {
getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => { getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params); getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params);
const teamId = requireTeamId(req?.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({
monitorId: validatedParams.monitorId, monitorId: req.params.monitorId as string,
teamId, teamId,
}); });
@@ -114,11 +96,11 @@ class MaintenanceWindowController implements IMaintenanceWindowController {
}; };
deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = deleteMaintenanceWindowByIdParamValidation.parse(req.params); deleteMaintenanceWindowByIdParamValidation.parse(req.params);
const teamId = requireTeamId(req?.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: validatedParams.id, teamId }); await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id as string, teamId });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -131,14 +113,14 @@ class MaintenanceWindowController implements IMaintenanceWindowController {
editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = editMaintenanceWindowByIdParamValidation.parse(req.params); editMaintenanceWindowByIdParamValidation.parse(req.params);
const validatedBody = editMaintenanceByIdWindowBodyValidation.parse(req.body); editMaintenanceByIdWindowBodyValidation.parse(req.body);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({
id: validatedParams.id, id: req.params.id as string,
body: validatedBody, body: req.body,
teamId, teamId,
}); });
return res.status(200).json({ return res.status(200).json({
+122 -136
View File
@@ -1,8 +1,9 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
import { import {
getMonitorByIdParamValidation, getMonitorByIdParamValidation,
getMonitorByIdQueryValidation, getMonitorByIdQueryValidation,
getMonitorsByTeamIdParamValidation,
getMonitorsByTeamIdQueryValidation, getMonitorsByTeamIdQueryValidation,
getMonitorsWithChecksQueryValidation, getMonitorsWithChecksQueryValidation,
createMonitorBodyValidation, createMonitorBodyValidation,
@@ -11,48 +12,31 @@ import {
getCertificateParamValidation, getCertificateParamValidation,
getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation, getHardwareDetailsByIdQueryValidation,
getUptimeDetailsByIdParamValidation,
getUptimeDetailsByIdQueryValidation,
importMonitorsBodyValidation,
} from "@/validation/monitorValidation.js"; } from "@/validation/monitorValidation.js";
import sslChecker from "ssl-checker"; import sslChecker from "ssl-checker";
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "@/controllers/controllerUtils.js"; import {
fetchMonitorCertificate,
requireString,
optionalString,
optionalNumber,
optionalBoolean,
parseMonitorTypeFilter,
parseSortOrder,
requireTeamId,
requireUserId,
} from "./controllerUtils.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { IMonitorService, INotificationsService } from "@/service/index.js"; import { IMonitorService } from "@/service/index.js";
import { GeoContinent } from "@/types/geoCheck.js"; import { GeoContinent } from "@/types/geoCheck.js";
const SERVICE_NAME = "monitorController"; const SERVICE_NAME = "monitorController";
class MonitorController {
export interface IMonitorController {
getMonitorCertificate: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getUptimeDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getHardwareDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getPageSpeedDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getGeoChecksByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getMonitorById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
createMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
importMonitorsFromJSON: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
deleteMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
deleteAllMonitors: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
editMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
pauseMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
addDemoMonitors: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getMonitorsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getMonitorsWithChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
exportMonitorsToJSON: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getAllGames: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getGroupsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
updateNotifications: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class MonitorController implements IMonitorController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private monitorService: IMonitorService; private monitorService: IMonitorService;
private notificationsService: INotificationsService;
constructor(monitorService: IMonitorService, notificationsService: INotificationsService) { constructor(monitorService: IMonitorService) {
this.monitorService = monitorService; this.monitorService = monitorService;
this.notificationsService = notificationsService;
} }
get serviceName() { get serviceName() {
@@ -61,9 +45,10 @@ class MonitorController implements IMonitorController {
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getCertificateParamValidation.parse(req.params); getCertificateParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId }); const monitorId = requireString(req.params?.monitorId, "Monitor ID");
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
const certificate = await fetchMonitorCertificate(sslChecker, monitor); const certificate = await fetchMonitorCertificate(sslChecker, monitor);
return res.status(200).json({ return res.status(200).json({
@@ -80,18 +65,16 @@ class MonitorController implements IMonitorController {
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => { getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params); const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query); const dateRange = requireString(req?.query?.dateRange, "dateRange");
const normalize = optionalBoolean(req?.query?.normalize, "normalize");
const monitorId = validatedParams.monitorId; const teamId = requireTeamId(req?.user?.teamId);
const dateRange = validatedQuery.dateRange;
const teamId = requireTeamId(req.user?.teamId);
const data = await this.monitorService.getUptimeDetailsById({ const data = await this.monitorService.getUptimeDetailsById({
teamId, teamId,
monitorId, monitorId,
dateRange, dateRange,
normalize,
}); });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -105,12 +88,12 @@ class MonitorController implements IMonitorController {
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => { getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); getHardwareDetailsByIdParamValidation.parse(req.params);
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); getHardwareDetailsByIdQueryValidation.parse(req.query);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const dateRange = validatedQuery.dateRange || "recent"; const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent";
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const data = await this.monitorService.getHardwareDetailsById({ const data = await this.monitorService.getHardwareDetailsById({
teamId, teamId,
@@ -129,12 +112,12 @@ class MonitorController implements IMonitorController {
}; };
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => { getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); getHardwareDetailsByIdParamValidation.parse(req.params);
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); getHardwareDetailsByIdQueryValidation.parse(req.query);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const dateRange = validatedQuery.dateRange || "recent"; const dateRange = requireString(req?.query?.dateRange, "dateRange");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const data = await this.monitorService.getPageSpeedDetailsById({ const data = await this.monitorService.getPageSpeedDetailsById({
teamId, teamId,
@@ -154,14 +137,18 @@ class MonitorController implements IMonitorController {
getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => { getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMonitorByIdParamValidation.parse(req.params); getMonitorByIdParamValidation.parse(req.params);
const validatedQuery = getMonitorByIdQueryValidation.parse(req.query); getMonitorByIdQueryValidation.parse(req.query);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const dateRange = validatedQuery.dateRange || "recent"; const dateRange = requireString(req?.query?.dateRange, "dateRange");
const continentParam = validatedQuery.continent; const continentParam = req?.query?.continent;
const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined; const continents = continentParam
const teamId = requireTeamId(req.user?.teamId); ? Array.isArray(continentParam)
? (continentParam as GeoContinent[])
: [continentParam as GeoContinent]
: undefined;
const teamId = requireTeamId(req?.user?.teamId);
const data = await this.monitorService.getGeoChecksByMonitorId({ const data = await this.monitorService.getGeoChecksByMonitorId({
teamId, teamId,
@@ -182,10 +169,11 @@ class MonitorController implements IMonitorController {
getMonitorById = async (req: Request, res: Response, next: NextFunction) => { getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMonitorByIdParamValidation.parse(req.params); getMonitorByIdParamValidation.parse(req.params);
getMonitorByIdQueryValidation.parse(req.query);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
@@ -201,12 +189,12 @@ class MonitorController implements IMonitorController {
createMonitor = async (req: Request, res: Response, next: NextFunction) => { createMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = createMonitorBodyValidation.parse(req.body); createMonitorBodyValidation.parse(req.body);
const userId = requireUserId(req.user?.id); const userId = requireString(req?.user?.id, "User ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody); const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -220,10 +208,14 @@ class MonitorController implements IMonitorController {
importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => { importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const userId = requireUserId(req.user?.id); const userId = requireUserId(req?.user?.id);
const validatedBody = importMonitorsBodyValidation.parse(req.body);
const monitors = validatedBody.monitors; const { monitors } = req.body;
if (!monitors || !Array.isArray(monitors)) {
throw new AppError({ message: "Invalid request: monitors array is required", status: 400 });
}
const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors }); const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors });
@@ -239,9 +231,9 @@ class MonitorController implements IMonitorController {
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMonitorByIdParamValidation.parse(req.params); getMonitorByIdParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId); const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const monitorId = validatedParams.monitorId; const teamId = requireTeamId(req?.user?.teamId);
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
@@ -257,7 +249,8 @@ class MonitorController implements IMonitorController {
deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => { deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
return res.status(200).json({ return res.status(200).json({
@@ -271,12 +264,12 @@ class MonitorController implements IMonitorController {
editMonitor = async (req: Request, res: Response, next: NextFunction) => { editMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMonitorByIdParamValidation.parse(req.params); getMonitorByIdParamValidation.parse(req.params);
const validatedBody = editMonitorBodyValidation.parse(req.body); editMonitorBodyValidation.parse(req.body);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody }); const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -290,10 +283,10 @@ class MonitorController implements IMonitorController {
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => { pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = pauseMonitorParamValidation.parse(req.params); pauseMonitorParamValidation.parse(req.params);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
@@ -309,8 +302,8 @@ class MonitorController implements IMonitorController {
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const id = requireUserId(req.user?.id); const id = requireString(req?.user?.id, "User ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId });
return res.status(200).json({ return res.status(200).json({
@@ -323,13 +316,32 @@ class MonitorController implements IMonitorController {
} }
}; };
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
try {
const { to } = req.body;
if (!to || typeof to !== "string") {
throw new AppError({ message: "Invalid 'to' email address", status: 400 });
}
const messageId = await this.monitorService.sendTestEmail({ to });
return res.status(200).json({
success: true,
msg: "Test email sent successfully",
data: { messageId },
});
} catch (error) {
next(error);
}
};
getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); getMonitorsByTeamIdParamValidation.parse(req.params);
getMonitorsByTeamIdQueryValidation.parse(req.query);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const type = validatedQuery.type; const type = parseMonitorTypeFilter(req.query?.type);
const filter = validatedQuery.filter; const filter = optionalString(req.query?.filter, "filter");
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter }); const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
@@ -345,16 +357,17 @@ class MonitorController implements IMonitorController {
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query); getMonitorsByTeamIdParamValidation.parse(req.params);
getMonitorsWithChecksQueryValidation.parse(req.query);
const limit = validatedQuery.limit; const explain = optionalBoolean(req?.query?.explain, "explain");
const page = validatedQuery.page; const limit = optionalNumber(req?.query?.limit, "limit");
const rowsPerPage = validatedQuery.rowsPerPage; const page = optionalNumber(req?.query?.page, "page");
const filter = validatedQuery.filter; const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage");
const field = validatedQuery.field; const filter = optionalString(req?.query?.filter, "filter");
const order = validatedQuery.order; const field = optionalString(req?.query?.field, "field");
const type = validatedQuery.type; const order = parseSortOrder(req?.query?.order);
const teamId = requireTeamId(req.user?.teamId); const type = parseMonitorTypeFilter(req?.query?.type);
const teamId = requireTeamId(req?.user?.teamId);
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
teamId, teamId,
@@ -365,6 +378,7 @@ class MonitorController implements IMonitorController {
filter, filter,
field, field,
order, order,
explain,
}); });
return res.status(200).json({ return res.status(200).json({
@@ -378,7 +392,11 @@ class MonitorController implements IMonitorController {
exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const json = await this.monitorService.exportMonitorsToJSON({ teamId }); const json = await this.monitorService.exportMonitorsToJSON({ teamId });
return res.status(200).json({ return res.status(200).json({
@@ -406,7 +424,11 @@ class MonitorController implements IMonitorController {
getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const groups = await this.monitorService.getGroupsByTeamId({ teamId }); const groups = await this.monitorService.getGroupsByTeamId({ teamId });
return res.status(200).json({ return res.status(200).json({
@@ -417,42 +439,6 @@ class MonitorController implements IMonitorController {
next(error); next(error);
} }
}; };
updateNotifications = async (req: Request, res: Response, next: NextFunction) => {
try {
updateNotificationsValidation.parse(req.body);
const teamId = requireTeamId(req.user?.teamId);
const { monitorIds, notificationIds, action } = req.body;
// Verify all requested notification IDs actually belong to this team
const teamNotifications = await this.notificationsService.findNotificationsByTeamId(teamId);
const validNotificationIds = teamNotifications.map((n) => n.id);
const invalidIds = notificationIds.filter((id: string) => !validNotificationIds.includes(id));
if (invalidIds.length > 0) {
throw new AppError({
message: `The following notification IDs are invalid or do not belong to your team: ${invalidIds.join(", ")}`,
status: 403,
});
}
const modifiedCount = await this.monitorService.updateNotifications({
teamId,
monitorIds,
notificationIds,
action,
});
return res.status(200).json({
success: true,
msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`,
data: { modifiedCount },
});
} catch (error) {
next(error);
}
};
} }
export default MonitorController; export default MonitorController;
@@ -1,30 +1,16 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { import { Notification } from "@/types/index.js";
createNotificationBodyValidation, import { createNotificationBodyValidation } from "@/validation/notificationValidation.js";
deleteNotificationParamValidation,
getNotificationByIdParamValidation,
testNotificationBodyValidation,
editNotificationParamValidation,
testAllNotificationsBodyValidation,
} from "@/validation/notificationValidation.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { INotificationsService } from "@/service/index.js";
import { requireTeamId, requireUserId } from "./controllerUtils.js";
import { IMonitorsRepository } from "@/repositories/index.js"; import { IMonitorsRepository } from "@/repositories/index.js";
import { INotificationsService } from "@/service/index.js";
import { requireTeamId } from "./controllerUtils.js";
const SERVICE_NAME = "NotificationController"; const SERVICE_NAME = "NotificationController";
export interface INotificationController { class NotificationController {
testNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; static SERVICE_NAME = SERVICE_NAME;
createNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getNotificationsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
deleteNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getNotificationById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
editNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
testAllNotifications: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class NotificationController implements INotificationController {
private notificationsService: INotificationsService; private notificationsService: INotificationsService;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) { constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
@@ -32,9 +18,13 @@ class NotificationController implements INotificationController {
this.monitorsRepository = monitorsRepository; this.monitorsRepository = monitorsRepository;
} }
get serviceName() {
return NotificationController.SERVICE_NAME;
}
testNotification = async (req: Request, res: Response, next: NextFunction) => { testNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const notification = testNotificationBodyValidation.parse(req.body); const notification: Notification = req.body;
const success = await this.notificationsService.sendTestNotification(notification); const success = await this.notificationsService.sendTestNotification(notification);
if (!success) { if (!success) {
@@ -44,7 +34,6 @@ class NotificationController implements INotificationController {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification sent successfully", msg: "Notification sent successfully",
details: { service: SERVICE_NAME },
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -53,12 +42,23 @@ class NotificationController implements INotificationController {
createNotification = async (req: Request, res: Response, next: NextFunction) => { createNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = createNotificationBodyValidation.parse(req.body); createNotificationBodyValidation.parse(req.body);
const teamId = requireTeamId(req.user?.teamId); const body = req.body;
const userId = requireUserId(req.user?.id);
const notification = await this.notificationsService.createNotification(validatedBody, userId, teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const userId = req?.user?.id;
if (!userId) {
throw new AppError({ message: "User ID is required", status: 400 });
}
body.userId = userId;
body.teamId = teamId;
const notification = await this.notificationsService.createNotification(body);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification created successfully", msg: "Notification created successfully",
@@ -71,7 +71,11 @@ class NotificationController implements INotificationController {
getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const notifications = await this.notificationsService.findNotificationsByTeamId(teamId); const notifications = await this.notificationsService.findNotificationsByTeamId(teamId);
return res.status(200).json({ return res.status(200).json({
@@ -86,10 +90,17 @@ class NotificationController implements INotificationController {
deleteNotification = async (req: Request, res: Response, next: NextFunction) => { deleteNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
const validatedParams = deleteNotificationParamValidation.parse(req.params); if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
await this.notificationsService.deleteById(validatedParams.id, teamId); const notificationId = req.params.id as string;
if (!notificationId) {
throw new AppError({ message: "Notification ID is required", status: 400 });
}
await this.notificationsService.deleteById(notificationId, teamId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification deleted successfully", msg: "Notification deleted successfully",
@@ -102,9 +113,12 @@ class NotificationController implements INotificationController {
getNotificationById = async (req: Request, res: Response, next: NextFunction) => { getNotificationById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const validatedParams = getNotificationByIdParamValidation.parse(req.params); const notificationId = req.params.id as string;
if (!notificationId) {
throw new AppError({ message: "Notification ID is required", status: 400 });
}
const notification = await this.notificationsService.findById(validatedParams.id, teamId); const notification = await this.notificationsService.findById(notificationId, teamId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -118,13 +132,14 @@ class NotificationController implements INotificationController {
editNotification = async (req: Request, res: Response, next: NextFunction) => { editNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = createNotificationBodyValidation.parse(req.body); createNotificationBodyValidation.parse(req.body);
const validatedParams = editNotificationParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const notificationId = validatedParams.id; const notificationId = req.params.id as string;
if (!notificationId) {
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody); throw new AppError({ message: "Notification ID is required", status: 400 });
}
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification updated successfully", msg: "Notification updated successfully",
@@ -137,13 +152,15 @@ class NotificationController implements INotificationController {
testAllNotifications = async (req: Request, res: Response, next: NextFunction) => { testAllNotifications = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = testAllNotificationsBodyValidation.parse(req.body); const monitorId = req.body.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const teamId = requireTeamId(req.user?.teamId); const monitor = await this.monitorsRepository.findById(monitorId, teamId);
const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId);
const notifications = monitor.notifications || [];
const notifications = monitor.notifications;
if (notifications.length === 0) { if (notifications.length === 0) {
throw new AppError({ message: "No notifications", status: 400 }); throw new AppError({ message: "No notifications", status: 400 });
} }
+28 -11
View File
@@ -1,19 +1,11 @@
import { ISuperSimpleQueue } from "@/service/index.js";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
const SERVICE_NAME = "JobQueueController"; const SERVICE_NAME = "JobQueueController";
export interface IJobQueueController { class JobQueueController {
getMetrics(req: Request, res: Response, next: NextFunction): Promise<void>;
getJobs(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
getAllMetrics(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
flushQueue(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
}
class JobQueueController implements IJobQueueController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private jobQueue: ISuperSimpleQueue; private jobQueue: any;
constructor(jobQueue: ISuperSimpleQueue) { constructor(jobQueue: any) {
this.jobQueue = jobQueue; this.jobQueue = jobQueue;
} }
@@ -61,6 +53,18 @@ class JobQueueController implements IJobQueueController {
} }
}; };
addJob = async (req: Request, res: Response, next: NextFunction) => {
try {
await this.jobQueue.addJob(Math.random().toString(36).substring(7));
return res.status(200).json({
success: true,
msg: "Job added to queue successfully",
});
} catch (error) {
next(error);
}
};
flushQueue = async (req: Request, res: Response, next: NextFunction) => { flushQueue = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const result = await this.jobQueue.flushQueues(); const result = await this.jobQueue.flushQueues();
@@ -73,5 +77,18 @@ class JobQueueController implements IJobQueueController {
next(error); next(error);
} }
}; };
checkQueueHealth = async (req: Request, res: Response, next: NextFunction) => {
try {
const stuckQueues = await this.jobQueue.checkQueueHealth();
return res.status(200).json({
success: true,
msg: "Queue health checked successfully",
data: stuckQueues,
});
} catch (error) {
next(error);
}
};
} }
export default JobQueueController; export default JobQueueController;
+9 -21
View File
@@ -2,23 +2,14 @@ import { Request, Response, NextFunction } from "express";
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js"; import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js"; import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { IEmailService, ISettingsService } from "@/service/index.js";
import { Settings } from "@/types/settings.js";
const SERVICE_NAME = "SettingsController"; const SERVICE_NAME = "SettingsController";
export interface ISettingsController { class SettingsController {
serviceName: string;
getAppSettings(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
updateAppSettings(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
sendTestEmail(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
}
class SettingsController implements ISettingsController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private settingsService: ISettingsService; private settingsService: any;
private emailService: IEmailService; private emailService: any;
constructor(settingsService: ISettingsService, emailService: IEmailService) { constructor(settingsService: any, emailService: any) {
this.settingsService = settingsService; this.settingsService = settingsService;
this.emailService = emailService; this.emailService = emailService;
} }
@@ -27,11 +18,11 @@ class SettingsController implements ISettingsController {
return SettingsController.SERVICE_NAME; return SettingsController.SERVICE_NAME;
} }
buildAppSettings = (dbSettings: Settings) => { buildAppSettings = (dbSettings: any) => {
const sanitizedSettings: Record<string, unknown> = { ...dbSettings }; const sanitizedSettings = { ...dbSettings };
delete sanitizedSettings.version; delete sanitizedSettings.version;
delete sanitizedSettings.jwtSecret; delete sanitizedSettings.jwtSecret;
const returnSettings: Record<string, unknown | null> = { const returnSettings = {
pagespeedKeySet: false, pagespeedKeySet: false,
emailPasswordSet: false, emailPasswordSet: false,
settings: null, settings: null,
@@ -66,9 +57,9 @@ class SettingsController implements ISettingsController {
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = updateAppSettingsBodyValidation.parse(req.body); updateAppSettingsBodyValidation.parse(req.body);
const updatedSettings = await this.settingsService.updateDbSettings(validatedBody); const updatedSettings = await this.settingsService.updateDbSettings(req.body);
const returnSettings = this.buildAppSettings(updatedSettings); const returnSettings = this.buildAppSettings(updatedSettings);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -104,9 +95,6 @@ class SettingsController implements ISettingsController {
const context = { testName: "Monitoring System" }; const context = { testName: "Monitoring System" };
const html = await this.emailService.buildEmail("testEmailTemplate", context); const html = await this.emailService.buildEmail("testEmailTemplate", context);
if (!html) {
throw new AppError({ message: "Failed to build email template.", status: 500 });
}
const messageId = await this.emailService.sendEmail(to, subject, html, { const messageId = await this.emailService.sendEmail(to, subject, html, {
systemEmailHost, systemEmailHost,
systemEmailPort, systemEmailPort,
+2 -11
View File
@@ -15,16 +15,7 @@ import { NormalizeData } from "@/utils/dataUtils.js";
const SERVICE_NAME = "statusPageController"; const SERVICE_NAME = "statusPageController";
export interface IStatusPageController { class StatusPageController {
readonly serviceName: string;
createStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
updateStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
getStatusPageByUrl(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
getStatusPagesByTeamId(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
deleteStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
}
class StatusPageController implements IStatusPageController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private statusPageService: IStatusPageService; private statusPageService: IStatusPageService;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
@@ -75,7 +66,7 @@ class StatusPageController implements IStatusPageController {
if (statusPage === null) { if (statusPage === null) {
throw new AppError({ message: "Status page not found", status: 404 }); throw new AppError({ message: "Status page not found", status: 404 });
} }
res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Status page updated successfully", msg: "Status page updated successfully",
data: statusPage, data: statusPage,
@@ -1,5 +1,4 @@
import mongoose from "mongoose"; import mongoose from "mongoose";
import type { Document, AnyBulkWriteOperation } from "mongodb";
const CHECKS_COLLECTION = "checks"; const CHECKS_COLLECTION = "checks";
const BACKUP_COLLECTION = "checks_backup"; const BACKUP_COLLECTION = "checks_backup";
@@ -86,7 +85,7 @@ const createTimeSeriesCollection = async (backedUp: boolean) => {
} }
}; };
const validateDocument = (doc: Document): boolean => { const validateDocument = (doc: any): boolean => {
if (!doc.createdAt) { if (!doc.createdAt) {
return false; return false;
} }
@@ -96,7 +95,7 @@ const validateDocument = (doc: Document): boolean => {
return true; return true;
}; };
const storeFailedDocument = async (doc: Document, reason: string) => { const storeFailedDocument = async (doc: any, reason: string) => {
const db = getDb(); const db = getDb();
await db.collection(FAILED_DOCS_COLLECTION).insertOne({ await db.collection(FAILED_DOCS_COLLECTION).insertOne({
originalDoc: doc, originalDoc: doc,
@@ -125,8 +124,8 @@ const migrateBackupData = async (backedUp: boolean): Promise<MigrationStats> =>
stats.totalSource = await source.countDocuments(); stats.totalSource = await source.countDocuments();
const cursor = source.find().addCursorFlag("noCursorTimeout", true); const cursor = source.find().addCursorFlag("noCursorTimeout", true);
const operations: AnyBulkWriteOperation<Document>[] = []; const operations: any[] = [];
const invalidDocs: Document[] = []; const invalidDocs: any[] = [];
while (await cursor.hasNext()) { while (await cursor.hasNext()) {
const doc = await cursor.next(); const doc = await cursor.next();
@@ -140,7 +139,6 @@ const migrateBackupData = async (backedUp: boolean): Promise<MigrationStats> =>
} }
const { _id, monitorId, teamId, type, ...rest } = doc; const { _id, monitorId, teamId, type, ...rest } = doc;
void _id;
const metadata = { monitorId, teamId, type }; const metadata = { monitorId, teamId, type };
operations.push({ insertOne: { document: { ...rest, metadata } } }); operations.push({ insertOne: { document: { ...rest, metadata } } });
@@ -42,10 +42,10 @@ export async function cleanupDuplicateMonitorStats(): Promise<void> {
const monitorId = duplicate._id; const monitorId = duplicate._id;
const { ids, updatedAts } = duplicate; const { ids, updatedAts } = duplicate;
type DocPair = { id: string; updatedAt: Date }; type DocPair = { id: any; updatedAt: Date };
// Create array of {id, updatedAt} pairs and sort by updatedAt descending // Create array of {id, updatedAt} pairs and sort by updatedAt descending
const docs: DocPair[] = ids.map((id: string, index: number) => ({ const docs: DocPair[] = ids.map((id: any, index: number) => ({
id, id,
updatedAt: updatedAts[index], updatedAt: updatedAts[index],
})); }));
@@ -37,8 +37,7 @@ 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) {
const value = monitor.get(field); if ((monitor as any)[field] === 0 || (monitor as any)[field] === 5) {
if (value === 0 || value === 5) {
update[field] = 100; update[field] = 100;
} }
} }
@@ -2,6 +2,12 @@ import mongoose from "mongoose";
import { logger } from "@/utils/logger.js"; import { logger } from "@/utils/logger.js";
import StatusPageModel from "../models/StatusPage.js"; import StatusPageModel from "../models/StatusPage.js";
/**
* Migration: Convert StatusPage `type` field from string to array of strings
*
* Old format: { type: "uptime" }
* New format: { type: ["uptime"] }
*/
export async function migrateStatusPageTypeToArray(): Promise<void> { export async function migrateStatusPageTypeToArray(): Promise<void> {
const SERVICE_NAME = "Migration:MigrateStatusPageTypeToArray"; const SERVICE_NAME = "Migration:MigrateStatusPageTypeToArray";
@@ -13,6 +19,7 @@ export async function migrateStatusPageTypeToArray(): Promise<void> {
throw new Error("Database connection is not initialized"); throw new Error("Database connection is not initialized");
} }
// Find all status pages where `type` is a string (not yet migrated)
const result = await StatusPageModel.updateMany( const result = await StatusPageModel.updateMany(
{ {
$expr: { $expr: {
+2 -3
View File
@@ -4,7 +4,6 @@ import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStat
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 { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js";
import type { ILogger } from "@/utils/logger.js";
type MigrationEntry = { type MigrationEntry = {
name: string; name: string;
@@ -19,7 +18,7 @@ const migrations: MigrationEntry[] = [
{ name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray }, { name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray },
]; ];
const runMigrations = async (logger?: ILogger) => { const runMigrations = async (logger?: { info: Function; error: Function }) => {
try { try {
logger?.info({ message: "Running migrations", service: "Migrations" }); logger?.info({ message: "Running migrations", service: "Migrations" });
for (const migration of migrations) { for (const migration of migrations) {
@@ -46,7 +45,7 @@ const runMigrations = async (logger?: ILogger) => {
logger?.info({ message: "Migrations completed", service: "Migrations" }); logger?.info({ message: "Migrations completed", service: "Migrations" });
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
logger?.error({ message: "Migration failed", service: "Migrations", details: { error: err?.message }, stack: err?.stack }); logger?.error({ message: "Migration failed", service: "Migrations", details: err?.message, stack: err?.stack });
throw error; throw error;
} }
}; };
+1 -1
View File
@@ -1,5 +1,5 @@
import { Schema, model, type Types } from "mongoose"; import { Schema, model, type Types } from "mongoose";
import type { MaintenanceWindow } from "@/types/maintenanceWindow.js"; import type { MaintenanceWindow, DurationUnit } from "@/types/maintenanceWindow.js";
type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt"> & { type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt"> & {
monitorId: Types.ObjectId; monitorId: Types.ObjectId;
+2 -168
View File
@@ -1,18 +1,6 @@
import { Schema, model, Types } from "mongoose"; import { Schema, model, Types, type UpdateQuery } from "mongoose";
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js"; import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js"; import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
import type {
CheckAudits,
CheckCaptureInfo,
CheckCpuInfo,
CheckDiskInfo,
CheckErrorInfo,
CheckHostInfo,
CheckMemoryInfo,
CheckNetworkInterfaceInfo,
GotTimings,
ILighthouseAudit,
} from "@/types/check.js";
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date }; type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
@@ -35,167 +23,13 @@ interface MonitorDocument extends MonitorDocumentBase {
updatedAt: Date; updatedAt: Date;
} }
const snapshotTimingPhasesSchema = new Schema<GotTimings["phases"]>(
{
wait: { type: Number },
dns: { type: Number },
tcp: { type: Number },
tls: { type: Number },
request: { type: Number },
firstByte: { type: Number },
download: { type: Number },
total: { type: Number },
},
{ _id: false }
);
const snapshotTimingsSchema = new Schema<GotTimings>(
{
start: { type: Number },
socket: { type: Number },
lookup: { type: Number },
connect: { type: Number },
secureConnect: { type: Number },
upload: { type: Number },
response: { type: Number },
end: { type: Number },
phases: { type: snapshotTimingPhasesSchema },
},
{ _id: false }
);
const snapshotCpuSchema = new Schema<CheckCpuInfo>(
{
physical_core: { type: Number },
logical_core: { type: Number },
frequency: { type: Number },
current_frequency: { type: Number },
temperature: { type: [Number] },
free_percent: { type: Number },
usage_percent: { type: Number },
},
{ _id: false }
);
const snapshotMemorySchema = new Schema<CheckMemoryInfo>(
{
total_bytes: { type: Number },
available_bytes: { type: Number },
used_bytes: { type: Number },
usage_percent: { type: Number },
},
{ _id: false }
);
const snapshotDiskSchema = new Schema<CheckDiskInfo>(
{
device: { type: String },
mountpoint: { type: String },
total_bytes: { type: Number },
free_bytes: { type: Number },
used_bytes: { type: Number },
usage_percent: { type: Number },
total_inodes: { type: Number },
free_inodes: { type: Number },
used_inodes: { type: Number },
inodes_usage_percent: { type: Number },
read_bytes: { type: Number },
write_bytes: { type: Number },
read_time: { type: Number },
write_time: { type: Number },
},
{ _id: false }
);
const snapshotHostSchema = new Schema<CheckHostInfo>(
{
os: { type: String },
platform: { type: String },
kernel_version: { type: String },
pretty_name: { type: String },
},
{ _id: false }
);
const snapshotErrorSchema = new Schema<CheckErrorInfo>(
{
metric: { type: [String] },
err: { type: String },
},
{ _id: false }
);
const snapshotCaptureSchema = new Schema<CheckCaptureInfo>(
{
version: { type: String },
mode: { type: String },
},
{ _id: false }
);
const snapshotNetworkInterfaceSchema = new Schema<CheckNetworkInterfaceInfo>(
{
name: { type: String },
bytes_sent: { type: Number },
bytes_recv: { type: Number },
packets_sent: { type: Number },
packets_recv: { type: Number },
err_in: { type: Number },
err_out: { type: Number },
drop_in: { type: Number },
drop_out: { type: Number },
fifo_in: { type: Number },
fifo_out: { type: Number },
},
{ _id: false }
);
const snapshotLighthouseAuditSchema = new Schema<ILighthouseAudit>(
{
id: { type: String },
title: { type: String },
score: { type: Number },
displayValue: { type: String },
numericValue: { type: Number },
numericUnit: { type: String },
},
{ _id: false }
);
const snapshotAuditsSchema = new Schema<CheckAudits>(
{
cls: { type: snapshotLighthouseAuditSchema },
si: { type: snapshotLighthouseAuditSchema },
fcp: { type: snapshotLighthouseAuditSchema },
lcp: { type: snapshotLighthouseAuditSchema },
tbt: { type: snapshotLighthouseAuditSchema },
},
{ _id: false }
);
const checkSnapshotSchema = new Schema<CheckSnapshotDocument>( const checkSnapshotSchema = new Schema<CheckSnapshotDocument>(
{ {
id: { type: String, required: true }, id: { type: String, required: true },
status: { type: Boolean, required: true }, status: { type: Boolean, required: true },
responseTime: { type: Number },
timings: { type: snapshotTimingsSchema },
statusCode: { type: Number },
message: { type: String },
cpu: { type: snapshotCpuSchema },
memory: { type: snapshotMemorySchema },
disk: { type: [snapshotDiskSchema] },
host: { type: snapshotHostSchema },
errors: { type: [snapshotErrorSchema] },
capture: { type: snapshotCaptureSchema },
net: { type: [snapshotNetworkInterfaceSchema] },
accessibility: { type: Number },
bestPractices: { type: Number },
seo: { type: Number },
performance: { type: Number },
audits: { type: snapshotAuditsSchema },
createdAt: { type: Date, required: true }, createdAt: { type: Date, required: true },
}, },
{ _id: false } { _id: false, strict: false }
); );
const MonitorSchema = new Schema<MonitorDocument>( const MonitorSchema = new Schema<MonitorDocument>(
+1 -1
View File
@@ -45,7 +45,7 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
type: { type: {
type: [String], type: [String],
required: true, required: true,
default: ["uptime"], default: [],
enum: StatusPageTypes, enum: StatusPageTypes,
}, },
companyName: { companyName: {
+6 -9
View File
@@ -1,18 +1,15 @@
import type { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from "express";
import { logger } from "@/utils/logger.js"; import { logger } from "@/utils/logger.js";
import { AppError } from "@/utils/AppError.js";
const handleErrors = (error: unknown, req: Request, res: Response, _next: NextFunction) => { const handleErrors = (error: any, req: Request, res: Response, next: NextFunction) => {
const status = error instanceof AppError ? error.status || 500 : 500; const status = error.status || 500;
const message = error instanceof AppError ? error.message : "Server error"; const message = error.message || "Server error";
const service = error instanceof AppError ? error.service : "unknownService"; const service = error.service || "unknownService";
const method = error instanceof AppError ? error.method : "unknownMethod";
logger.error({ logger.error({
message: message, message: message,
service: service, service: service,
method: method, method: error.method,
stack: error instanceof AppError ? error.stack : undefined, stack: error.stack,
details: error instanceof AppError ? error.details : undefined,
}); });
res.status(status).json({ res.status(status).json({
status, status,
+5 -12
View File
@@ -1,9 +1,7 @@
import type { Request, Response, NextFunction } from "express"; import type { Request, Response, NextFunction } from "express";
import DOMPurify from "isomorphic-dompurify"; import DOMPurify from "isomorphic-dompurify";
export function sanitizeInput(input: string, options?: object): string; export const sanitizeInput = (input: any, options = {}) => {
export function sanitizeInput(input: unknown, options?: object): unknown;
export function sanitizeInput(input: unknown, options = {}): unknown {
if (typeof input !== "string") { if (typeof input !== "string") {
return input; return input;
} }
@@ -17,9 +15,9 @@ export function sanitizeInput(input: unknown, options = {}): unknown {
}; };
return DOMPurify.sanitize(input, defaultConfig); return DOMPurify.sanitize(input, defaultConfig);
} };
export const sanitizeObject = (obj: unknown, options = {}): unknown => { export const sanitizeObject = (obj: Record<string, any>, options = {}): Record<string, any> => {
if (typeof obj !== "object" || obj === null) { if (typeof obj !== "object" || obj === null) {
return obj; return obj;
} }
@@ -28,7 +26,7 @@ export const sanitizeObject = (obj: unknown, options = {}): unknown => {
return obj.map((item) => sanitizeObject(item, options)); return obj.map((item) => sanitizeObject(item, options));
} }
const sanitized: Record<string, unknown> = {}; const sanitized: Record<string, any> = {};
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
if (typeof value === "string") { if (typeof value === "string") {
sanitized[key] = sanitizeInput(value, options); sanitized[key] = sanitizeInput(value, options);
@@ -54,12 +52,7 @@ export const sanitizeBody = (options = {}): ((req: Request, res: Response, next:
export const sanitizeQuery = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => { export const sanitizeQuery = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => {
return (req: Request, res: Response, next: NextFunction) => { return (req: Request, res: Response, next: NextFunction) => {
if (req.query && typeof req.query === "object") { if (req.query && typeof req.query === "object") {
for (const key of Object.keys(req.query)) { req.query = sanitizeObject(req.query, options);
const value = req.query[key];
if (typeof value === "string") {
req.query[key] = sanitizeInput(value, options);
}
}
} }
next(); next();
}; };
+7 -13
View File
@@ -2,17 +2,12 @@ import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import type { ISettingsService } from "@/service/system/settingsService.js"; import { Settings } from "@/types/settings.js";
import type { User } from "@/types/user.js";
const SERVICE_NAME = "verifyJWT"; const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer "; const TOKEN_PREFIX = "Bearer ";
const isUser = (payload: unknown): payload is User => { export const createVerifyJWT = (settingsService: { getSettings: () => Settings }) => {
return typeof payload === "object" && payload !== null && "id" in payload && "teamId" in payload && "role" in payload;
};
export const createVerifyJWT = (settingsService: ISettingsService) => {
return (req: Request, res: Response, next: NextFunction) => { return (req: Request, res: Response, next: NextFunction) => {
const token = req.headers["authorization"]; const token = req.headers["authorization"];
// Make sure a token is provided // Make sure a token is provided
@@ -36,22 +31,21 @@ export const createVerifyJWT = (settingsService: ISettingsService) => {
next(error); next(error);
return; return;
} }
jwt.verify(parsedToken, jwtSecret, (err: jwt.VerifyErrors | null, decoded: jwt.JwtPayload | string | undefined) => { jwt.verify(parsedToken, jwtSecret, (err: any, decoded: any) => {
if (err) { if (err) {
const error = new AppError({ const error = new AppError({
message: err instanceof Error ? err.message : "Failed to authenticate token", message: "Failed to authenticate token",
details: err,
status: 401, status: 401,
service: SERVICE_NAME, service: SERVICE_NAME,
details: err instanceof Error ? { error: err } : undefined,
method: "verifyJWT", method: "verifyJWT",
}); });
next(error); next(error);
return; return;
} else if (isUser(decoded)) { } else {
// Token is valid, carry on
req.user = decoded; req.user = decoded;
next(); next();
} else {
next(new AppError({ message: "Invalid token payload", status: 401, service: SERVICE_NAME, method: "verifyJWT" }));
} }
}); });
}; };
@@ -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 { GeoCheckMetadataDocument, GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js"; import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
import mongoose, { PipelineStage } from "mongoose"; import mongoose from "mongoose";
import { getDateForRange } from "@/utils/dataUtils.js"; import { getDateForRange } from "@/utils/dataUtils.js";
import { ILogger } from "@/utils/logger.js"; 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: GeoCheckMetadataDocument): GeoCheckMetadata => ({ const mapMetadata = (metadata: any): GeoCheckMetadata => ({
monitorId: toStringId(metadata.monitorId), monitorId: toStringId(metadata.monitorId),
teamId: toStringId(metadata.teamId), teamId: toStringId(metadata.teamId),
type: metadata.type, type: metadata.type,
}); });
const mapResults = (results: GeoCheckResult[]): GeoCheckResult[] => { const mapResults = (results: any[]): GeoCheckResult[] => {
if (!results || !Array.isArray(results)) { if (!results || !Array.isArray(results)) {
return []; return [];
} }
@@ -89,12 +89,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
})) }))
); );
return docs.map((doc) => this.toEntity(doc)); return docs.map((doc) => this.toEntity(doc));
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Failed to createGeoChecks: ${error.message}` : "Failed to createGeoChecks", message: `Failed to createGeoChecks: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "createGeoChecks", method: "createGeoChecks",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
throw error; throw error;
} }
@@ -109,7 +109,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
continents?: GeoContinent[] continents?: GeoContinent[]
): Promise<FlatGeoChecksQueryResult> => { ): Promise<FlatGeoChecksQueryResult> => {
try { try {
const matchStage: Record<string, unknown> = { const matchStage: Record<string, any> = {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId), "metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
...(getDateForRange(dateRange) && { ...(getDateForRange(dateRange) && {
createdAt: { createdAt: {
@@ -125,8 +125,22 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
skip = page * rowsPerPage; skip = page * rowsPerPage;
} }
if (continents && continents.length > 0) {
const pipeline: any[] = [
{ $match: matchStage },
{ $unwind: "$results" },
{
$match: {
"results.location.continent": { $in: continents },
},
},
];
} else {
const pipeline: any[] = [{ $match: matchStage }, { $unwind: "$results" }];
}
// Common pipeline stages for both paths // Common pipeline stages for both paths
const pipeline: PipelineStage[] = [ const pipeline: any[] = [
{ $match: matchStage }, { $match: matchStage },
{ $unwind: "$results" }, { $unwind: "$results" },
// Filter by continent if specified // Filter by continent if specified
@@ -160,7 +174,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
]; ];
// Count pipeline // Count pipeline
const countPipeline: PipelineStage[] = [ const countPipeline: any[] = [
{ $match: matchStage }, { $match: matchStage },
{ $unwind: "$results" }, { $unwind: "$results" },
...(continents && continents.length > 0 ...(continents && continents.length > 0
@@ -192,12 +206,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
})); }));
return { geoChecksCount, geoChecks }; return { geoChecksCount, geoChecks };
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Error finding geo checks by monitor ID: ${error.message}` : "Error finding geo checks by monitor ID", message: `Error finding geo checks by monitor ID: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "findByMonitorId", method: "findByMonitorId",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
throw error; throw error;
} }
@@ -213,15 +227,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
}, },
}).sort({ createdAt: -1 }); }).sort({ createdAt: -1 });
return docs.map(this.toEntity); return docs.map(this.toEntity);
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: message: `Error finding geo checks by monitor ID and date range: ${error.message}`,
error instanceof Error
? `Error finding geo checks by monitor ID and date range: ${error.message}`
: "Error finding geo checks by monitor ID and date range",
service: SERVICE_NAME, service: SERVICE_NAME,
method: "findByMonitorIdAndDateRange", method: "findByMonitorIdAndDateRange",
stack: error instanceof Error ? error.stack : undefined,
}); });
throw error; throw error;
} }
@@ -235,7 +245,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
continents?: GeoContinent[] continents?: GeoContinent[]
): Promise<GroupedGeoCheck[]> => { ): Promise<GroupedGeoCheck[]> => {
try { try {
const pipeline: PipelineStage[] = [ const pipeline: any[] = [
// Match geo checks for this monitor in date range // Match geo checks for this monitor in date range
{ {
$match: { $match: {
@@ -315,12 +325,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
const results = await GeoCheckModel.aggregate(pipeline); const results = await GeoCheckModel.aggregate(pipeline);
return results as GroupedGeoCheck[]; return results as GroupedGeoCheck[];
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Error finding grouped geo checks: ${error.message}` : "Error finding grouped geo checks", message: `Error finding grouped geo checks: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "findGroupedByMonitorIdAndDateRange", method: "findGroupedByMonitorIdAndDateRange",
stack: error instanceof Error ? error.stack : undefined,
}); });
throw error; throw error;
} }
@@ -332,12 +341,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId), "metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
}); });
return result.deletedCount || 0; return result.deletedCount || 0;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Error deleting geo checks by monitor ID: ${error.message}` : "Error deleting geo checks by monitor ID", message: `Error deleting geo checks by monitor ID: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "deleteByMonitorId", method: "deleteByMonitorId",
stack: error instanceof Error ? error.stack : undefined,
}); });
throw error; throw error;
} }
@@ -349,12 +357,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
"metadata.teamId": new mongoose.Types.ObjectId(teamId), "metadata.teamId": new mongoose.Types.ObjectId(teamId),
}); });
return result.deletedCount || 0; return result.deletedCount || 0;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Error deleting geo checks by team ID: ${error.message}` : "Error deleting geo checks by team ID", message: `Error deleting geo checks by team ID: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "deleteByTeamId", method: "deleteByTeamId",
stack: error instanceof Error ? error.stack : undefined,
}); });
throw error; throw error;
} }
@@ -365,12 +372,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } }); const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } });
return result.deletedCount || 0; return result.deletedCount || 0;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Error deleting orphaned geo checks: ${error.message}` : "Error deleting orphaned geo checks", message: `Error deleting orphaned geo checks: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "deleteByMonitorIdsNotIn", method: "deleteByMonitorIdsNotIn",
stack: error instanceof Error ? error.stack : undefined,
}); });
throw error; throw error;
} }
@@ -32,8 +32,8 @@ class MongoIncidentRepository implements IIncidentsRepository {
status?: boolean; status?: boolean;
monitorId?: string; monitorId?: string;
resolutionType?: string; resolutionType?: string;
}): Record<string, unknown> { }): Record<string, any> {
const matchStage: Record<string, unknown> = { const matchStage: Record<string, any> = {
teamId: new mongoose.Types.ObjectId(teamId), teamId: new mongoose.Types.ObjectId(teamId),
...(status !== undefined && { status }), ...(status !== undefined && { status }),
...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }), ...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }),
@@ -5,12 +5,12 @@ export interface IMaintenanceWindowsRepository {
// fetch // fetch
findById(id: string, teamId: string): Promise<MaintenanceWindow>; findById(id: string, teamId: string): Promise<MaintenanceWindow>;
findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>; findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>;
findByTeamId(teamId: string, page: number, rowsPerPage: number, field?: string, order?: string, active?: boolean): Promise<MaintenanceWindow[]>; findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise<MaintenanceWindow[]>;
// update // update
updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>; updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>;
// delete // delete
deleteById(id: string, teamId: string): Promise<MaintenanceWindow>; deleteById(id: string, teamId: string): Promise<MaintenanceWindow>;
// other // other
countByTeamId(teamId: string, active?: boolean): Promise<number>; countByTeamId(teamId: string, active: boolean): Promise<number>;
} }
@@ -1,7 +1,7 @@
import type { MaintenanceWindow } from "@/types/index.js"; import type { MaintenanceWindow } from "@/types/index.js";
import { type MaintenanceWindowDocument, MaintenanceWindowModel } from "@/db/models/index.js"; import { type MaintenanceWindowDocument, MaintenanceWindowModel } from "@/db/models/index.js";
import { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js"; import { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js";
import mongoose, { SortOrder } from "mongoose"; import mongoose from "mongoose";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository { class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository {
@@ -75,13 +75,13 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
findByTeamId = async ( findByTeamId = async (
teamId: string, teamId: string,
active: boolean,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
field?: string, field: string,
order?: string, order: string
active?: boolean
): Promise<MaintenanceWindow[]> => { ): Promise<MaintenanceWindow[]> => {
const maintenanceQuery: Record<string, unknown> = { teamId }; const maintenanceQuery: Record<string, any> = { teamId };
if (active !== undefined) maintenanceQuery.active = active; if (active !== undefined) maintenanceQuery.active = active;
@@ -92,7 +92,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
} }
// Sorting // Sorting
const sort: Record<string, SortOrder> = {}; const sort: Record<string, any> = {};
if (field !== undefined && order !== undefined) { if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1; sort[field] = order === "asc" ? 1 : -1;
} }
@@ -126,8 +126,8 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
} }
return this.toEntity(deleted); return this.toEntity(deleted);
}; };
countByTeamId = async (teamId: string, active?: boolean) => { countByTeamId = async (teamId: string, active: boolean) => {
const maintenanceQuery: Record<string, unknown> = { teamId }; const maintenanceQuery: Record<string, any> = { teamId };
if (active !== undefined) maintenanceQuery.active = active; if (active !== undefined) maintenanceQuery.active = active;
@@ -41,7 +41,6 @@ export interface IMonitorsRepository {
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>; findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>;
findGroupsByTeamId(teamId: string): Promise<string[]>; findGroupsByTeamId(teamId: string): Promise<string[]>;
removeNotificationFromMonitors(notificationId: string): Promise<void>; removeNotificationFromMonitors(notificationId: string): Promise<void>;
updateNotifications(teamId: string, monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set"): Promise<number>;
deleteByTeamIdsNotIn(teamIds: string[]): Promise<number>; deleteByTeamIdsNotIn(teamIds: string[]): Promise<number>;
findAllMonitorIds(): Promise<string[]>; findAllMonitorIds(): Promise<string[]>;
} }
@@ -1,9 +1,8 @@
import { MonitorModel } from "@/db/models/index.js"; import { MonitorModel } from "@/db/models/index.js";
import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js"; import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js";
import type { Monitor, MonitorsSummary, CheckSnapshot } from "@/types/index.js"; import type { Monitor, MonitorsSummary, CheckSnapshot, Check } from "@/types/index.js";
import mongoose, { type FilterQuery, type PipelineStage } from "mongoose"; import mongoose, { type FilterQuery, type PipelineStage } from "mongoose";
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
import { MongoBulkWriteError } from "mongodb";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
class MongoMonitorsRepository implements IMonitorsRepository { class MongoMonitorsRepository implements IMonitorsRepository {
@@ -21,9 +20,12 @@ class MongoMonitorsRepository implements IMonitorsRepository {
try { try {
const inserted = await MonitorModel.insertMany(payload, { ordered: false }); const inserted = await MonitorModel.insertMany(payload, { ordered: false });
return this.mapDocuments(inserted); return this.mapDocuments(inserted);
} catch (error: unknown) { } catch (error: any) {
if (error instanceof MongoBulkWriteError && "insertedDocs" in error && Array.isArray(error.insertedDocs) && error.insertedDocs.length > 0) { if (error.name === "MongoBulkWriteError" || error.name === "BulkWriteError") {
return this.mapDocuments(error.insertedDocs); const insertedDocs = error.insertedDocs || [];
if (insertedDocs.length > 0) {
return this.mapDocuments(insertedDocs);
}
} }
throw error; throw error;
} }
@@ -44,7 +46,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
}; };
findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => { findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => {
const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type } = config ?? {}; const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type, limit } = config ?? {};
const query: Record<string, unknown> = { const query: Record<string, unknown> = {
teamId: new mongoose.Types.ObjectId(teamId), teamId: new mongoose.Types.ObjectId(teamId),
@@ -296,41 +298,6 @@ class MongoMonitorsRepository implements IMonitorsRepository {
await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } }); await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } });
}; };
updateNotifications = async (
teamId: string,
monitorIds: string[],
notificationIds: string[],
action: "add" | "remove" | "set"
): Promise<number> => {
let objectIds;
let notificationObjectIds;
try {
objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
notificationObjectIds = notificationIds.map((id) => new mongoose.Types.ObjectId(id));
} catch {
throw new AppError({ message: "One or more monitor or notification IDs are invalid", status: 400 });
}
const filter = { _id: { $in: objectIds }, teamId: new mongoose.Types.ObjectId(teamId) };
let update;
switch (action) {
case "add":
update = { $addToSet: { notifications: { $each: notificationObjectIds } } };
break;
case "remove":
update = { $pull: { notifications: { $in: notificationObjectIds } } };
break;
case "set":
update = { $set: { notifications: notificationObjectIds } };
break;
default:
throw new AppError({ message: `Invalid action: ${action}`, status: 400 });
}
const result = await MonitorModel.updateMany(filter, update);
return result.modifiedCount;
};
private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { private mapDocuments = (documents: MonitorDocument[]): Monitor[] => {
if (!documents?.length) { if (!documents?.length) {
return []; return [];
@@ -387,7 +354,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
gameId: doc.gameId ?? undefined, gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined,
group: doc.group ?? null, group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [], geoCheckLocations: doc.geoCheckLocations ?? [],
geoCheckInterval: doc.geoCheckInterval ?? 300000, geoCheckInterval: doc.geoCheckInterval ?? 300000,
@@ -396,7 +363,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
}; };
}; };
private toEntityWithChecks = (doc: MonitorDocument): Monitor => { private toEntityWithChecks = (doc: any): Monitor => {
const toStringId = (value: unknown): string => { const toStringId = (value: unknown): string => {
if (value instanceof mongoose.Types.ObjectId) { if (value instanceof mongoose.Types.ObjectId) {
return value.toString(); return value.toString();
@@ -446,7 +413,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
gameId: doc.gameId ?? undefined, gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined,
group: doc.group ?? null, group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [], geoCheckLocations: doc.geoCheckLocations ?? [],
geoCheckInterval: doc.geoCheckInterval ?? 300000, geoCheckInterval: doc.geoCheckInterval ?? 300000,
@@ -1,11 +1,11 @@
import type { Settings, SettingsUpdate } from "@/types/index.js"; import type { Settings } from "@/types/index.js";
export interface ISettingsRepository { export interface ISettingsRepository {
// create // create
create(settings: Partial<Settings>): Promise<Settings>; create(settings: Partial<Settings>): Promise<Settings>;
// fetch // fetch
findSingleton(): Promise<Settings | null>; findSingleton(): Promise<Settings | null>;
// update // update
update(settings: SettingsUpdate): Promise<Settings>; update(settings: Partial<Settings>): Promise<Settings>;
// delete // delete
deleteLegacy: () => Promise<boolean>; deleteLegacy: () => Promise<boolean>;
} }
@@ -1,6 +1,6 @@
import mongoose, { type UpdateQuery } from "mongoose"; import mongoose from "mongoose";
import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js"; import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js";
import type { Settings, SettingsUpdate } from "@/types/index.js"; import type { Settings } from "@/types/index.js";
import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js"; import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js";
class MongoSettingsRepository implements ISettingsRepository { class MongoSettingsRepository implements ISettingsRepository {
@@ -59,23 +59,27 @@ class MongoSettingsRepository implements ISettingsRepository {
return this.toEntity(settings); return this.toEntity(settings);
}; };
update = async (settings: SettingsUpdate) => { update = async (settings: Partial<Settings>) => {
const $set: Record<string, unknown> = {}; const update: Record<string, any> = { $set: {}, $unset: {} };
const $unset: Record<string, string> = {};
// Iterate through settings and separate into $set and $unset // Iterate through settings and separate into $set and $unset
Object.entries(settings).forEach(([key, value]) => { Object.entries(settings).forEach(([key, value]) => {
if (value === undefined || value === null) { if (value === undefined || value === "") {
$unset[key] = ""; // Unset undefined or empty string values
update.$unset[key] = "";
} else { } else {
$set[key] = value; // Set defined values
update.$set[key] = value;
} }
}); });
const update: UpdateQuery<AppSettingsDocument> = { // Clean up empty operators
...(Object.keys($set).length > 0 && { $set }), if (Object.keys(update.$set).length === 0) {
...(Object.keys($unset).length > 0 && { $unset }), delete update.$set;
}; }
if (Object.keys(update.$unset).length === 0) {
delete update.$unset;
}
await AppSettingsModel.findOneAndUpdate({}, update, { await AppSettingsModel.findOneAndUpdate({}, update, {
upsert: true, upsert: true,
@@ -72,8 +72,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
}; };
create = async (userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => { create = async (userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => {
const { logo, ...restData } = data; const { logo: _logo, ...restData } = data;
void logo;
const statusPage = new StatusPageModel({ const statusPage = new StatusPageModel({
...restData, ...restData,
userId, userId,
@@ -111,8 +110,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
image: Express.Multer.File | undefined, image: Express.Multer.File | undefined,
patch: Partial<StatusPage> & { removeLogo?: string } patch: Partial<StatusPage> & { removeLogo?: string }
): Promise<StatusPage> => { ): Promise<StatusPage> => {
const { logo, removeLogo, ...restPatch } = patch; const { logo: _logo, removeLogo, ...restPatch } = patch;
void logo;
const updateData: StatusPageUpdateData = { ...restPatch }; const updateData: StatusPageUpdateData = { ...restPatch };
if (image) { if (image) {
updateData.logo = { updateData.logo = {
@@ -117,7 +117,7 @@ class MongoUsersRepository implements IUsersRepository {
delete candidateUser.deleteProfileImage; delete candidateUser.deleteProfileImage;
const updateQuery: Record<string, unknown> = { $set: candidateUser }; const updateQuery: Record<string, any> = { $set: candidateUser };
if (unsetFields) { if (unsetFields) {
updateQuery.$unset = unsetFields; updateQuery.$unset = unsetFields;
} }
+2 -3
View File
@@ -1,15 +1,14 @@
import { Router, RequestHandler } from "express"; import { Router, RequestHandler } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import multer from "multer"; import multer from "multer";
import { IAuthController } from "@/controllers/authController.js";
const upload = multer(); const upload = multer();
class AuthRoutes { class AuthRoutes {
private router: Router; private router: Router;
private authController: IAuthController; private authController: any;
constructor(authController: IAuthController, verifyJWT: RequestHandler) { constructor(authController: any, verifyJWT: RequestHandler) {
this.router = Router(); this.router = Router();
this.authController = authController; this.authController = authController;
this.initRoutes(verifyJWT); this.initRoutes(verifyJWT);
+2 -3
View File
@@ -1,13 +1,12 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { ICheckController } from "@/controllers/checkController.js";
class CheckRoutes { class CheckRoutes {
private router: Router; private router: Router;
private checkController: ICheckController; private checkController: any;
constructor(checkController: ICheckController) { constructor(checkController: any) {
this.router = Router(); this.router = Router();
this.checkController = checkController; this.checkController = checkController;
this.initRoutes(); this.initRoutes();
+2 -3
View File
@@ -1,12 +1,11 @@
import { RequestHandler, Router } from "express"; import { RequestHandler, Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { IDiagnosticController } from "@/controllers/diagnosticController.js";
class DiagnosticRoutes { class DiagnosticRoutes {
private router: Router; private router: Router;
private diagnosticController: IDiagnosticController; private diagnosticController: any;
constructor(diagnosticController: IDiagnosticController, verifyJWT: RequestHandler) { constructor(diagnosticController: any, verifyJWT: RequestHandler) {
this.router = Router(); this.router = Router();
this.diagnosticController = diagnosticController; this.diagnosticController = diagnosticController;
this.initRoutes(verifyJWT); this.initRoutes(verifyJWT);
+2 -3
View File
@@ -1,11 +1,10 @@
import { IGeoCheckController } from "@/controllers/geoCheckController.js";
import { Router } from "express"; import { Router } from "express";
class GeoCheckRoutes { class GeoCheckRoutes {
private router: Router; private router: Router;
private geoCheckController: IGeoCheckController; private geoCheckController: any;
constructor(geoCheckController: IGeoCheckController) { constructor(geoCheckController: any) {
this.router = Router(); this.router = Router();
this.geoCheckController = geoCheckController; this.geoCheckController = geoCheckController;
this.initRoutes(); this.initRoutes();
+2 -3
View File
@@ -1,12 +1,11 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { IIncidentController } from "@/controllers/incidentController.js";
class IncidentRoutes { class IncidentRoutes {
private router: Router; private router: Router;
private incidentController: IIncidentController; private incidentController: any;
constructor(incidentController: IIncidentController) { constructor(incidentController: any) {
this.router = Router(); this.router = Router();
this.incidentController = incidentController; this.incidentController = incidentController;
this.initRoutes(); this.initRoutes();
+2 -3
View File
@@ -1,12 +1,11 @@
import { RequestHandler, Router } from "express"; import { RequestHandler, Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { IInviteController } from "@/controllers/inviteController.js";
class InviteRoutes { class InviteRoutes {
private router: Router; private router: Router;
private inviteController: IInviteController; private inviteController: any;
constructor(inviteController: IInviteController, verifyJWT: RequestHandler) { constructor(inviteController: any, verifyJWT: RequestHandler) {
this.router = Router(); this.router = Router();
this.inviteController = inviteController; this.inviteController = inviteController;
this.initRoutes(verifyJWT); this.initRoutes(verifyJWT);
+3 -4
View File
@@ -1,11 +1,10 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "@/middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { ILogController } from "@/controllers/logController.js";
class LogRoutes { class LogRoutes {
private router: Router; private router: Router;
private logController: ILogController; private logController: any;
constructor(logController: ILogController) { constructor(logController: any) {
this.router = Router(); this.router = Router();
this.logController = logController; this.logController = logController;
this.initRoutes(); this.initRoutes();
+2 -3
View File
@@ -1,11 +1,10 @@
import { IMaintenanceWindowController } from "@/controllers/maintenanceWindowController.js";
import { Router } from "express"; import { Router } from "express";
class MaintenanceWindowRoutes { class MaintenanceWindowRoutes {
private router: Router; private router: Router;
private mwController: IMaintenanceWindowController; private mwController: any;
constructor(maintenanceWindowController: IMaintenanceWindowController) { constructor(maintenanceWindowController: any) {
this.router = Router(); this.router = Router();
this.mwController = maintenanceWindowController; this.mwController = maintenanceWindowController;
this.initRoutes(); this.initRoutes();
+9 -5
View File
@@ -1,11 +1,15 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "@/middleware/isAllowed.js"; import { isAllowed } from "@/middleware/isAllowed.js";
import { IMonitorController } from "@/controllers/monitorController.js"; import multer from "multer";
import { fetchMonitorCertificate } from "@/controllers/controllerUtils.js";
const upload = multer({
storage: multer.memoryStorage(), // Store file in memory as Buffer
});
class MonitorRoutes { class MonitorRoutes {
private router: Router; private router: Router;
private monitorController: IMonitorController; private monitorController: any;
constructor(monitorController: IMonitorController) { constructor(monitorController: any) {
this.router = Router(); this.router = Router();
this.monitorController = monitorController; this.monitorController = monitorController;
this.initRoutes(); this.initRoutes();
@@ -34,11 +38,10 @@ class MonitorRoutes {
// Util routes // Util routes
this.router.get("/certificate/:monitorId", (req, res, next) => { this.router.get("/certificate/:monitorId", (req, res, next) => {
this.monitorController.getMonitorCertificate(req, res, next); this.monitorController.getMonitorCertificate(req, res, next, fetchMonitorCertificate);
}); });
// General monitor CRUD routes // General monitor CRUD routes
this.router.patch("/notifications", isAllowed(["admin", "superadmin"]), this.monitorController.updateNotifications);
this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor); this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor);
this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors); this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors);
@@ -47,6 +50,7 @@ class MonitorRoutes {
this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON); this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON);
this.router.post("/import/json", isAllowed(["admin", "superadmin"]), this.monitorController.importMonitorsFromJSON); this.router.post("/import/json", isAllowed(["admin", "superadmin"]), this.monitorController.importMonitorsFromJSON);
this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail);
this.router.get("/games", this.monitorController.getAllGames); this.router.get("/games", this.monitorController.getAllGames);
// Individual monitor CRUD routes // Individual monitor CRUD routes
+2 -3
View File
@@ -1,10 +1,9 @@
import { INotificationController } from "@/controllers/notificationController.js";
import { Router } from "express"; import { Router } from "express";
class NotificationRoutes { class NotificationRoutes {
private router: Router; private router: Router;
private notificationController: INotificationController; private notificationController: any;
constructor(notificationController: INotificationController) { constructor(notificationController: any) {
this.router = Router(); this.router = Router();
this.notificationController = notificationController; this.notificationController = notificationController;
this.initializeRoutes(); this.initializeRoutes();
+5 -4
View File
@@ -1,19 +1,20 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "@/middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { IJobQueueController } from "@/controllers/queueController.js";
class QueueRoutes { class QueueRoutes {
private router: Router; private router: Router;
private queueController: IJobQueueController; private queueController: any;
constructor(queueController: IJobQueueController) { constructor(queueController: any) {
this.router = Router(); this.router = Router();
this.queueController = queueController; this.queueController = queueController;
this.initRoutes(); this.initRoutes();
} }
initRoutes() { initRoutes() {
this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs); this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs);
this.router.post("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.addJob);
this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics); this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics);
this.router.get("/health", isAllowed(["admin", "superadmin"]), this.queueController.checkQueueHealth);
this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics); this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics);
this.router.post("/flush", isAllowed(["admin", "superadmin"]), this.queueController.flushQueue); this.router.post("/flush", isAllowed(["admin", "superadmin"]), this.queueController.flushQueue);
} }
+2 -3
View File
@@ -1,12 +1,11 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { ISettingsController } from "@/controllers/settingsController.js";
class SettingsRoutes { class SettingsRoutes {
private router: Router; private router: Router;
private settingsController: ISettingsController; private settingsController: any;
constructor(settingsController: ISettingsController) { constructor(settingsController: any) {
this.router = Router(); this.router = Router();
this.settingsController = settingsController; this.settingsController = settingsController;
this.initRoutes(); this.initRoutes();
+2 -3
View File
@@ -1,13 +1,12 @@
import { IStatusPageController } from "@/controllers/statusPageController.js";
import { RequestHandler, Router } from "express"; import { RequestHandler, Router } from "express";
import multer from "multer"; import multer from "multer";
const upload = multer(); const upload = multer();
class StatusPageRoutes { class StatusPageRoutes {
private router: Router; private router: Router;
private statusPageController: IStatusPageController; private statusPageController: any;
constructor(statusPageController: IStatusPageController, verifyJWT: RequestHandler, verifyStatusPageAccess: RequestHandler) { constructor(statusPageController: any, verifyJWT: RequestHandler, verifyStatusPageAccess: RequestHandler) {
this.router = Router(); this.router = Router();
this.statusPageController = statusPageController; this.statusPageController = statusPageController;
this.initRoutes(verifyJWT, verifyStatusPageAccess); this.initRoutes(verifyJWT, verifyStatusPageAccess);
@@ -24,7 +24,8 @@ export class DiagnosticService implements IDiagnosticService {
* Clears performance marks after each measurement to prevent memory leaks. * Clears performance marks after each measurement to prevent memory leaks.
*/ */
const obs = new PerformanceObserver((items) => { const obs = new PerformanceObserver((items) => {
items.getEntries(); // Get the first entry but we don't need to store it
items.getEntries()[0];
performance.clearMarks(); performance.clearMarks();
}); });
obs.observe({ entryTypes: ["measure"] }); obs.observe({ entryTypes: ["measure"] });
@@ -1,7 +1,7 @@
import type { Monitor, GeoCheck } from "@/types/index.js"; import type { Monitor, GeoCheck } from "@/types/index.js";
import type { GeoCheckResult, GeoContinent } from "@/types/geoCheck.js"; import type { GeoCheckResult } from "@/types/geoCheck.js";
import { Types } from "mongoose"; import { Types } from "mongoose";
import type { FlatGeoChecksQueryResult, IGeoChecksRepository } from "@/repositories/index.js"; import type { 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,15 +13,7 @@ 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: { getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise<any>;
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 {
@@ -147,23 +139,7 @@ export class GeoChecksService implements IGeoChecksService {
return this.geoChecksRepository.createGeoChecks(geoChecks); return this.geoChecksRepository.createGeoChecks(geoChecks);
}; };
getGeoChecksByMonitor = async ({ getGeoChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => {
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",
@@ -191,9 +167,10 @@ 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 || 0; const parsedPage = page ? parseInt(page) : page;
const parsedRowsPerPage = rowsPerPage || 5; const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents); const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents);
return result; return result;
}; };
+31 -29
View File
@@ -2,6 +2,7 @@ const SERVICE_NAME = "incidentService";
import type { Monitor } from "@/types/monitor.js"; import type { Monitor } from "@/types/monitor.js";
import type { MonitorStatusResponse } from "@/types/network.js"; import type { MonitorStatusResponse } from "@/types/network.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { ParseBoolean } from "@/utils/utils.js";
import { getDateForRange } from "@/utils/dataUtils.js"; import { getDateForRange } from "@/utils/dataUtils.js";
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js"; import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
import type { Incident, IncidentSummary, User } from "@/types/index.js"; import type { Incident, IncidentSummary, User } from "@/types/index.js";
@@ -21,13 +22,13 @@ export interface IIncidentService {
teamId: string, teamId: string,
sortOrder: string, sortOrder: string,
dateRange: string, dateRange: string,
page: number, page: string,
rowsPerPage: number, rowsPerPage: string,
status: boolean | undefined, status: string,
monitorId: string | undefined, monitorId: string,
resolutionType: string | undefined resolutionType: string
): Promise<{ incidents: Incident[]; count: number }>; ): Promise<{ incidents: Incident[]; count: number }>;
getIncidentSummary(teamId: string, limit?: number): Promise<IncidentSummary>; getIncidentSummary(teamId: string, limit?: string): Promise<IncidentSummary>;
getIncidentById(incidentId: string, teamId: string): Promise<{ incident: Incident; monitor: Monitor; user: User | null }>; getIncidentById(incidentId: string, teamId: string): Promise<{ incident: Incident; monitor: Monitor; user: User | null }>;
} }
@@ -163,13 +164,13 @@ export class IncidentService implements IIncidentService {
}); });
return resolvedIncident; return resolvedIncident;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
service: SERVICE_NAME, service: SERVICE_NAME,
method: "resolveIncident", method: "resolveIncident",
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
details: { id: incidentId }, details: { id: incidentId },
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
throw error; throw error;
} }
@@ -179,11 +180,11 @@ export class IncidentService implements IIncidentService {
teamId: string, teamId: string,
sortOrder: string, sortOrder: string,
dateRange: string, dateRange: string,
page: number, page: string,
rowsPerPage: number, rowsPerPage: string,
status: boolean | undefined, status: string,
monitorId: string | undefined, monitorId: string,
resolutionType: string | undefined resolutionType: string
) => { ) => {
try { try {
if (!teamId) { if (!teamId) {
@@ -192,8 +193,9 @@ export class IncidentService implements IIncidentService {
const startDate = getDateForRange(dateRange); const startDate = getDateForRange(dateRange);
const parsedPage = page ?? 0; const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
const parsedRowsPerPage = rowsPerPage ?? 20; const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20;
const parsedStatus = typeof status === "undefined" ? undefined : ParseBoolean(status);
const incidents = await this.incidentsRepository.findByTeamId( const incidents = await this.incidentsRepository.findByTeamId(
teamId, teamId,
@@ -201,43 +203,43 @@ export class IncidentService implements IIncidentService {
parsedPage, parsedPage,
parsedRowsPerPage, parsedRowsPerPage,
sortOrder, sortOrder,
status, parsedStatus,
monitorId, monitorId,
resolutionType resolutionType
); );
const count = await this.incidentsRepository.countByTeamId(teamId, startDate, status, monitorId, resolutionType); const count = await this.incidentsRepository.countByTeamId(teamId, startDate, parsedStatus, monitorId, resolutionType);
return { incidents, count }; return { incidents, count };
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
service: SERVICE_NAME, service: SERVICE_NAME,
method: "getIncidentsByTeam", method: "getIncidentsByTeam",
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
details: { teamId }, details: { teamId },
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
throw error; throw error;
} }
}; };
getIncidentSummary = async (teamId: string, limit?: number) => { getIncidentSummary = async (teamId: string, limit?: string) => {
try { try {
if (!teamId) { if (!teamId) {
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentSummary", status: 400 }); throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentSummary", status: 400 });
} }
const parsedLimit = limit ?? 10; const parsedLimit = limit && Number.isFinite(parseInt(limit, 10)) ? parseInt(limit, 10) : 10;
const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit); const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit);
return summary; return summary;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
service: SERVICE_NAME, service: SERVICE_NAME,
method: "getIncidentSummary", method: "getIncidentSummary",
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
details: { teamId }, details: { teamId },
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
throw error; throw error;
} }
@@ -252,13 +254,13 @@ export class IncidentService implements IIncidentService {
user = await this.usersRepository.findById(incident.resolvedBy); user = await this.usersRepository.findById(incident.resolvedBy);
} }
return { incident, monitor, user }; return { incident, monitor, user };
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
service: SERVICE_NAME, service: SERVICE_NAME,
method: "getIncidentById", method: "getIncidentById",
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
details: { incidentId }, details: { incidentId },
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
throw error; throw error;
} }
+6 -26
View File
@@ -2,8 +2,6 @@ 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";
@@ -16,8 +14,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: ISettingsService; private settingsService: any;
private emailService: IEmailService; private emailService: any;
private invitesRepository: IInvitesRepository; private invitesRepository: IInvitesRepository;
constructor({ constructor({
@@ -26,8 +24,8 @@ export class InviteService implements IInviteService {
emailService, emailService,
}: { }: {
invitesRepository: IInvitesRepository; invitesRepository: IInvitesRepository;
settingsService: ISettingsService; settingsService: any;
emailService: IEmailService; emailService: any;
}) { }) {
this.invitesRepository = invitesRepository; this.invitesRepository = invitesRepository;
this.settingsService = settingsService; this.settingsService = settingsService;
@@ -59,16 +57,8 @@ export class InviteService implements IInviteService {
return inviteToken; return inviteToken;
}; };
sendInviteEmail = async ({ invite, firstName, userRoles }: { invite: Partial<Invite>; firstName: string; userRoles: UserRole[] }) => { sendInviteEmail = async ({ invite, firstName, userRoles }: { invite: Partial<Invite>; firstName: any; 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));
@@ -76,7 +66,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: "sendInviteEmail", method: "getInviteToken",
status: 403, status: 403,
}); });
} }
@@ -89,16 +79,6 @@ 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,29 +1,16 @@
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js"; import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
import type { DurationUnit, MaintenanceWindow } from "@/types/index.js"; import type { MaintenanceWindow } from "@/types/index.js";
import { ParseBoolean } from "@/utils/utils.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: { createMaintenanceWindow(params: { teamId: string; body: Partial<MaintenanceWindow> & { monitors: string[] } }): Promise<void>;
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;
active?: boolean; query: Record<string, string>;
page?: number;
rowsPerPage?: number;
field?: string;
order?: string;
}): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>; }): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>;
getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise<MaintenanceWindow[]>; getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise<MaintenanceWindow[]>;
deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise<MaintenanceWindow>; deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
@@ -50,28 +37,9 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
return MaintenanceWindowService.SERVICE_NAME; return MaintenanceWindowService.SERVICE_NAME;
} }
createMaintenanceWindow = async ({ createMaintenanceWindow = async ({ teamId, body }: { teamId: string; body: any }) => {
teamId, const monitorIds = body.monitors;
monitorIDs, const monitors = await this.monitorsRepository.findByIds(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);
@@ -84,17 +52,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: name, name: body.name,
active: active, active: body.active ? body.active : true,
duration: duration, duration: body.duration,
durationUnit: durationUnit, durationUnit: body.durationUnit,
repeat: repeat, repeat: body.repeat,
start: start, start: body.start,
end: end, end: body.end,
}); });
}); });
await Promise.all(dbTransactions); await Promise.all(dbTransactions);
@@ -104,25 +72,14 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
return await this.maintenanceWindowsRepository.findById(id, teamId); return await this.maintenanceWindowsRepository.findById(id, teamId);
}; };
getMaintenanceWindowsByTeamId = async ({ getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => {
teamId, let { active, page, rowsPerPage, field, order } = query || {};
active,
page,
rowsPerPage,
field,
order,
}: {
teamId: string;
active?: boolean;
page?: number;
rowsPerPage?: number;
field?: string;
order?: string;
}) => {
page = page ?? 0;
rowsPerPage = rowsPerPage ?? 10;
const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, page, rowsPerPage, field, order, active); active = typeof active === "undefined" ? undefined : ParseBoolean(active);
page = parseInt(page) || 0;
rowsPerPage = parseInt(rowsPerPage) || 10;
const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, active, page, rowsPerPage, field, order);
const maintenanceWindowCount = await this.maintenanceWindowsRepository.countByTeamId(teamId, active); const maintenanceWindowCount = await this.maintenanceWindowsRepository.countByTeamId(teamId, active);
return { maintenanceWindows, maintenanceWindowCount }; return { maintenanceWindows, maintenanceWindowCount };
}; };
@@ -135,7 +92,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: Partial<MaintenanceWindow> }) => { editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: any }) => {
return await this.maintenanceWindowsRepository.updateById(id, teamId, body); return await this.maintenanceWindowsRepository.updateById(id, teamId, body);
}; };
} }
+63 -79
View File
@@ -7,7 +7,6 @@ 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";
@@ -21,10 +20,7 @@ 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";
@@ -33,20 +29,15 @@ export interface IMonitorService {
readonly serviceName: string; readonly serviceName: string;
// create // create
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>; createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
createMonitors(monitors: Array<Monitor>): Promise<Monitor[] | null>; createMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>; addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
// read // read
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<UptimeDetailsResult>; getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): 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: { getGeoChecksByMonitorId(args: { teamId: string; monitorId: string; dateRange: string; continents?: GeoContinent[] }): Promise<any>;
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;
@@ -67,33 +58,32 @@ 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[]>;
// update // update
editMonitor(args: { teamId: string; monitorId: string; body: Partial<Monitor> }): Promise<Monitor>; editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise<Monitor>;
pauseMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>; pauseMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
// delete // delete
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
sendTestEmail(args: { to: string }): Promise<string>;
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>; exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: ImportedMonitor[] }): Promise<{ imported: number; errors: string[] }>; importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): 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: IEmailService; private emailService: any;
private logger: ILogger; private logger: any;
private games: GamesMap; private games: any;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
private checksRepository: IChecksRepository; private checksRepository: IChecksRepository;
private geoChecksRepository: IGeoChecksRepository; private geoChecksRepository: IGeoChecksRepository;
@@ -114,9 +104,9 @@ export class MonitorService implements IMonitorService {
incidentsRepository, incidentsRepository,
}: { }: {
jobQueue: ISuperSimpleQueue; jobQueue: ISuperSimpleQueue;
emailService: IEmailService; emailService: any;
logger: ILogger; logger: any;
games: GamesMap; games: any;
monitorsRepository: IMonitorsRepository; monitorsRepository: IMonitorsRepository;
checksRepository: IChecksRepository; checksRepository: IChecksRepository;
geoChecksRepository: IGeoChecksRepository; geoChecksRepository: IGeoChecksRepository;
@@ -174,7 +164,7 @@ export class MonitorService implements IMonitorService {
this.jobQueue.addJob(monitor.id, monitor); this.jobQueue.addJob(monitor.id, monitor);
}; };
createMonitors = async (monitors: Array<Monitor>): Promise<Monitor[] | null> => { createMonitors = async (monitors: Array<Monitor>, userId: string, teamId: string): 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" });
@@ -184,7 +174,7 @@ export class MonitorService implements IMonitorService {
return createdMonitors; return createdMonitors;
}; };
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<Monitor[]> => { addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<any[]> => {
const demoData = demoMonitorsData; const demoData = demoMonitorsData;
const monitors = demoData.map((monitor) => ({ const monitors = demoData.map((monitor) => ({
userId, userId,
@@ -205,10 +195,12 @@ 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) {
@@ -336,7 +328,7 @@ export class MonitorService implements IMonitorService {
monitorId: string; monitorId: string;
dateRange: string; dateRange: string;
continents?: GeoContinent[]; continents?: GeoContinent[];
}): Promise<GroupedGeoCheckResult> => { }): Promise<any> => {
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 });
@@ -389,6 +381,7 @@ export class MonitorService implements IMonitorService {
filter, filter,
field, field,
order, order,
explain,
}: { }: {
teamId: string; teamId: string;
limit?: number; limit?: number;
@@ -398,6 +391,7 @@ 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 });
@@ -436,81 +430,55 @@ export class MonitorService implements IMonitorService {
return groups; return groups;
}; };
editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Partial<Monitor> }) => { editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => {
const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body); const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body);
await this.jobQueue.updateJob(editedMonitor); await this.jobQueue.updateJob(editedMonitor);
return editedMonitor; return editedMonitor;
}; };
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);
if (monitor.isActive) { monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor);
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: unknown) => { await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
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 instanceof Error ? err.stack : undefined, stack: err.stack,
}); });
}); });
await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => { await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
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 instanceof Error ? err.stack : undefined, stack: err.stack,
}); });
}); });
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: unknown) => { await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: any) => {
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 instanceof Error ? err.stack : undefined, stack: err.stack,
}); });
}); });
await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId).catch((err: unknown) => { await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId).catch((err: any) => {
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 instanceof Error ? err.stack : undefined, stack: err.stack,
}); });
}); });
await this.geoChecksRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => { await this.geoChecksRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
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 instanceof Error ? err.stack : undefined, stack: err.stack,
}); });
}); });
@@ -528,12 +496,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: unknown) { } catch (error: any) {
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 instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
} }
}) })
@@ -541,7 +509,21 @@ export class MonitorService implements IMonitorService {
return deletedCount; return deletedCount;
}; };
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<Monitor[]> => { sendTestEmail = async ({ to }: { to: string }): Promise<string> => {
const subject = "Test email from Checkmate";
const context = { testName: "Monitoring System" };
const html = await this.emailService.buildEmail("testEmailTemplate", context);
const messageId = await this.emailService.sendEmail(to, subject, html);
if (!messageId) {
throw new AppError({ message: "Failed to send test email.", service: SERVICE_NAME, method: "sendTestEmail", status: 500 });
}
return messageId;
};
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<any[]> => {
const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
if (!monitors || monitors.length === 0) { if (!monitors || monitors.length === 0) {
@@ -558,21 +540,23 @@ export class MonitorService implements IMonitorService {
}: { }: {
teamId: string; teamId: string;
userId: string; userId: string;
monitors: ImportedMonitor[]; monitors: any[];
}): Promise<{ imported: number; errors: string[] }> => { }): Promise<{ imported: number; errors: string[] }> => {
const errors: string[] = []; const errors: string[] = [];
const cleanedMonitors: Monitor[] = monitors.map((monitor) => ({ const cleanedMonitors = monitors.map((monitor) => {
...monitor, const cleanData = { ...monitor };
id: "", delete cleanData.id;
teamId, delete cleanData._id;
userId, delete cleanData.createdAt;
recentChecks: [], delete cleanData.updatedAt;
createdAt: "", delete cleanData.recentChecks;
updatedAt: "", // Monitors must belong to current team
})); cleanData.teamId = teamId;
return cleanData;
});
const createdMonitors = await this.createMonitors(cleanedMonitors); const createdMonitors = await this.createMonitors(cleanedMonitors, userId, teamId);
if (!createdMonitors || createdMonitors.length === 0) { if (!createdMonitors || createdMonitors.length === 0) {
throw new AppError({ throw new AppError({
+2 -2
View File
@@ -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 as User, file); const newUser = await this.usersRepository.create(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 as User, file); const newUser = await this.usersRepository.create(userData, file);
this.logger.debug({ this.logger.debug({
message: "New user created by superadmin", message: "New user created by superadmin",
@@ -2,11 +2,7 @@ import { Monitor } from "@/types/monitor.js";
import jmespath from "jmespath"; import jmespath from "jmespath";
type JmesPath = typeof jmespath; type JmesPath = typeof jmespath;
export interface IAdvancedMatcher { export class AdvancedMatcher {
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 {
@@ -16,7 +12,7 @@ export class AdvancedMatcher implements IAdvancedMatcher {
return String(actual) === expected; // Default return String(actual) === expected; // Default
} }
validate<T>(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: T | undefined } { validate<T>(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: any } {
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" };
@@ -39,10 +35,10 @@ export class AdvancedMatcher implements IAdvancedMatcher {
}; };
} }
const isFalsy = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null; const isFalsey = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null;
return { return {
ok: !isFalsy, ok: !isFalsey,
message: !isFalsy ? "Success" : "Extracted value is falsy", message: !isFalsey ? "Success" : "Extracted value is falsey",
extracted: dataToValidate, extracted: dataToValidate,
}; };
} }
@@ -1,5 +1,5 @@
import { type Got, HTTPError, RequestError } from "got"; import { type Got, HTTPError, RequestError } from "got";
import { IAdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js"; import { AdvancedMatcher } 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: IAdvancedMatcher private advancedMatcher: AdvancedMatcher
) { ) {
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 Exceeded: ${monitor.name}`; const title = `Threshold Breach: ${monitor.name}`;
const summary = `Monitor "${monitor.name}" has exceeded one or more thresholds.`; const summary = `Monitor "${monitor.name}" has breached one or more thresholds.`;
const details = [`URL: ${monitor.url}`, `Status: Threshold exceeded`, `Type: ${monitor.type}`]; const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`];
const thresholds = this.extractThresholdBreaches(monitor, monitorStatusResponse); const thresholds = this.extractThresholdBreaches(monitor, monitorStatusResponse);
@@ -3,5 +3,5 @@ import type { NotificationMessage } from "@/types/notificationMessage.js";
export interface INotificationProvider { export interface INotificationProvider {
sendMessage: (notification: Notification, message: NotificationMessage) => Promise<boolean>; sendMessage: (notification: Notification, message: NotificationMessage) => Promise<boolean>;
sendTestAlert(notification: Partial<Notification>): Promise<boolean>; sendTestAlert(notification: Notification): Promise<boolean>;
} }
@@ -13,7 +13,7 @@ export class DiscordProvider implements INotificationProvider {
this.logger = logger; this.logger = logger;
} }
sendTestAlert = async (notification: Partial<Notification>) => { sendTestAlert = async (notification: Notification) => {
if (!notification.address) { if (!notification.address) {
return false; return false;
} }
@@ -14,7 +14,7 @@ export class EmailProvider implements INotificationProvider {
this.logger = logger; this.logger = logger;
} }
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> { async sendTestAlert(notification: Notification): Promise<boolean> {
const subject = "Test notification"; const subject = "Test notification";
const html = await buildTestEmail(this.emailService); const html = await buildTestEmail(this.emailService);
@@ -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 exceeded`; return `Monitor ${message.monitor.name} threshold breached`;
case "threshold_resolved": case "threshold_resolved":
return `Monitor ${message.monitor.name} thresholds resolved`; return `Monitor ${message.monitor.name} thresholds resolved`;
default: default:
@@ -13,7 +13,7 @@ export class MatrixProvider implements INotificationProvider {
this.logger = logger; this.logger = logger;
} }
sendTestAlert = async (notification: Partial<Notification>) => { sendTestAlert = async (notification: Notification) => {
const { homeserverUrl, accessToken, roomId } = notification; const { homeserverUrl, accessToken, roomId } = notification;
if (!homeserverUrl || !accessToken || !roomId) { if (!homeserverUrl || !accessToken || !roomId) {
return false; return false;
@@ -14,7 +14,7 @@ export class PagerDutyProvider implements INotificationProvider {
this.logger = logger; this.logger = logger;
} }
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> { async sendTestAlert(notification: Notification): Promise<boolean> {
try { try {
await got.post("https://events.pagerduty.com/v2/enqueue", { await got.post("https://events.pagerduty.com/v2/enqueue", {
json: { json: {
@@ -13,7 +13,7 @@ export class SlackProvider implements INotificationProvider {
this.logger = logger; this.logger = logger;
} }
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> { async sendTestAlert(notification: Notification): Promise<boolean> {
if (!notification.address) { if (!notification.address) {
return false; return false;
} }
@@ -65,7 +65,7 @@ export class TeamsProvider implements INotificationProvider {
this.logger = logger; this.logger = logger;
} }
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> { async sendTestAlert(notification: Notification): Promise<boolean> {
if (!notification.address) { if (!notification.address) {
return false; return false;
} }
@@ -98,7 +98,7 @@ export class WebhookProvider implements INotificationProvider {
}; };
} }
sendTestAlert = async (notification: Partial<Notification>) => { sendTestAlert = async (notification: Notification) => {
if (!notification.address) { if (!notification.address) {
return false; return false;
} }
@@ -8,14 +8,14 @@ import { ILogger } from "@/utils/logger.js";
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js"; import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
export interface INotificationsService { export interface INotificationsService {
createNotification: (notificationData: Partial<Notification>, userId: string, teamId: string) => Promise<Notification>; createNotification: (notificationData: Partial<Notification>) => Promise<Notification>;
findById: (id: string, teamId: string) => Promise<Notification>; findById: (id: string, teamId: string) => Promise<Notification>;
findNotificationsByTeamId: (teamId: string) => Promise<Notification[]>; findNotificationsByTeamId: (teamId: string) => Promise<Notification[]>;
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>; updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
deleteById: (id: string, teamId: string) => Promise<Notification>; deleteById: (id: string, teamId: string) => Promise<Notification>;
handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise<boolean>; handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise<boolean>;
sendTestNotification: (notification: Partial<Notification>) => Promise<boolean>; sendTestNotification: (notification: Notification) => Promise<boolean>;
testAllNotifications: (notificationIds: string[]) => Promise<boolean>; testAllNotifications: (notificationIds: string[]) => Promise<boolean>;
} }
@@ -141,7 +141,7 @@ export class NotificationsService implements INotificationsService {
return await this.sendNotifications(monitor, monitorStatusResponse, decision); return await this.sendNotifications(monitor, monitorStatusResponse, decision);
}; };
sendTestNotification = async (notification: Partial<Notification>) => { sendTestNotification = async (notification: Notification) => {
switch (notification.type) { switch (notification.type) {
case "email": case "email":
return await this.emailProvider.sendTestAlert(notification); return await this.emailProvider.sendTestAlert(notification);
@@ -174,9 +174,7 @@ export class NotificationsService implements INotificationsService {
return true; return true;
}; };
createNotification = async (notificationData: Partial<Notification>, userId: string, teamId: string): Promise<Notification> => { createNotification = async (notificationData: Partial<Notification>): Promise<Notification> => {
notificationData.userId = userId;
notificationData.teamId = teamId;
return await this.notificationsRepository.create(notificationData); return await this.notificationsRepository.create(notificationData);
}; };
@@ -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 = "initializing"; monitor.status = newStatus;
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,7 +360,8 @@ export class StatusService implements IStatusService {
}; };
} catch (error: unknown) { } catch (error: unknown) {
throw new AppError({ throw new AppError({
message: `Failed to update monitor with id ${check.metadata.monitorId} with status: ${error instanceof Error ? error.message : "Unknown error"}`, message: `Failed to update monitor 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",
}); });
+2 -3
View File
@@ -1,5 +1,5 @@
import { ISettingsRepository } from "@/repositories/index.js"; import { ISettingsRepository } from "@/repositories/index.js";
import { Settings, SettingsUpdate } from "@/types/index.js"; import { Settings } from "@/types/index.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { ValidatedEnv } from "@/validation/envValidation.js"; import { ValidatedEnv } from "@/validation/envValidation.js";
import type { StringValue } from "ms"; import type { StringValue } from "ms";
@@ -19,7 +19,6 @@ 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 {
@@ -54,7 +53,7 @@ export class SettingsService implements ISettingsService {
return this.settings; return this.settings;
} }
updateDbSettings = async (newSettings: SettingsUpdate) => { updateDbSettings = async (newSettings: Partial<Settings>) => {
return await this.settingsRepository.update(newSettings); return await this.settingsRepository.update(newSettings);
}; };
+2 -7
View File
@@ -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, GroupedGeoCheck } from "@/types/geoCheck.js"; import type { GeoContinent } from "@/types/geoCheck.js";
export type { GeoContinent } from "@/types/geoCheck.js"; export type { GeoContinent } from "@/types/geoCheck.js";
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const; export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const;
@@ -12,8 +12,7 @@ export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupporte
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const; export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
export type MonitorStatus = (typeof MonitorStatuses)[number]; export type MonitorStatus = (typeof MonitorStatuses)[number];
export const MonitorMatchMethods = ["equal", "include", "regex"] as const; export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
export type MonitorMatchMethod = (typeof MonitorMatchMethods)[number] | "";
export interface Monitor { export interface Monitor {
id: string; id: string;
@@ -74,10 +73,6 @@ export interface MonitorsWithChecksByTeamIdResult {
monitors: Monitor[]; monitors: Monitor[];
} }
export interface GroupedGeoCheckResult {
groupedGeoChecks: GroupedGeoCheck[];
}
export interface UptimeDetailsResult { export interface UptimeDetailsResult {
monitorData: { monitorData: {
monitor: Monitor; monitor: Monitor;
-4
View File
@@ -5,10 +5,6 @@ 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;
+9 -9
View File
@@ -1,18 +1,18 @@
export interface AppErrorConfig { export interface AppErrorConfig {
message?: string; message: string;
status?: number; status?: number;
service?: string; service?: string | null;
method?: string; method?: string | null;
details?: Record<string, unknown> | undefined; details?: unknown;
} }
export class AppError extends Error { export class AppError extends Error {
readonly status: number; private status: number;
readonly service: string; private service: string | null;
readonly method: string; private method: string | null;
readonly details: Record<string, unknown> | undefined; private details: unknown;
constructor({ message, status = 500, service = "unknownService", method = "unknownMethod", details = undefined }: AppErrorConfig) { constructor({ message, status = 500, service = null, method = null, details = null }: AppErrorConfig) {
super(message); super(message);
this.status = status; this.status = status;
this.service = service; this.service = service;
+2 -3
View File
@@ -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 = typeof check.responseTime === "number" ? check.responseTime : 0; const originalResponseTime = check.responseTime;
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,8 +59,7 @@ export const NormalizeData = <T extends HasResponseTime>(checks: T[], rangeMin:
return normalizedChecks; return normalizedChecks;
} else { } else {
return checks.map((check) => { return checks.map((check) => {
const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0; return { ...check, originalResponseTime: check.responseTime };
return { ...check, originalResponseTime: originalResponseTime };
}); });
} }
}; };
@@ -1,20 +0,0 @@
import { z } from "zod";
import { booleanCoercion } from "./shared.js";
//****************************************
// Incident Validations
//****************************************
export const getIncidentsByTeamQueryValidation = z.object({
sortOrder: z.enum(["asc", "desc"]),
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
page: z.coerce.number().int().min(0),
rowsPerPage: z.coerce.number().int().min(1),
status: booleanCoercion.optional(),
monitorId: z.string().optional(),
resolutionType: z.enum(["manual", "automatic"]).optional(),
});
export const getIncidentSummaryQueryValidation = z.object({
limit: z.coerce.number().int().min(1).optional(),
});
-1
View File
@@ -20,4 +20,3 @@ export * from "./settingsValidation.js";
export * from "./statusPageValidation.js"; export * from "./statusPageValidation.js";
export * from "./notificationValidation.js"; export * from "./notificationValidation.js";
export * from "./userValidation.js"; export * from "./userValidation.js";
export * from "./incidentValidation.js";
@@ -1,8 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { booleanCoercion } from "./shared.js"; import { booleanCoercion } from "./shared.js";
const dateToString = z.coerce.date().transform((d) => d.toISOString());
//**************************************** //****************************************
// Maintenance Window Validations // Maintenance Window Validations
//**************************************** //****************************************
@@ -13,10 +11,10 @@ export const createMaintenanceWindowBodyValidation = z.object({
active: z.boolean().optional(), active: z.boolean().optional(),
duration: z.number().min(1, "Duration is required"), duration: z.number().min(1, "Duration is required"),
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]), durationUnit: z.enum(["seconds", "minutes", "hours", "days"]),
start: dateToString, start: z.coerce.date(),
end: dateToString, end: z.coerce.date(),
repeat: z.number().min(0, "Repeat must be a non-negative number"), repeat: z.number().min(0, "Repeat must be a non-negative number"),
expiry: dateToString.optional(), expiry: z.coerce.date().optional(),
}); });
export const getMaintenanceWindowByIdParamValidation = z.object({ export const getMaintenanceWindowByIdParamValidation = z.object({
@@ -47,10 +45,10 @@ export const editMaintenanceByIdWindowBodyValidation = z.object({
active: z.boolean().optional(), active: z.boolean().optional(),
name: z.string().optional(), name: z.string().optional(),
repeat: z.number().optional(), repeat: z.number().optional(),
start: dateToString.optional(), start: z.coerce.date().optional(),
end: dateToString.optional(), end: z.coerce.date().optional(),
expiry: dateToString.optional(), expiry: z.coerce.date().optional(),
monitors: z.array(z.string()).optional(), monitors: z.array(z.unknown()).optional(),
duration: z.number().optional(), duration: z.number().optional(),
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(), durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(),
}); });
+9 -67
View File
@@ -1,7 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { booleanCoercion } from "./shared.js"; import { booleanCoercion } from "./shared.js";
import { GeoContinents } from "@/types/geoCheck.js"; import { GeoContinents } from "@/types/geoCheck.js";
import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js";
export const getMonitorByIdParamValidation = z.object({ export const getMonitorByIdParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"), monitorId: z.string().min(1, "Monitor ID is required"),
@@ -14,7 +13,7 @@ export const getMonitorByIdQueryValidation = z.object({
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
numToDisplay: z.coerce.number().optional(), numToDisplay: z.coerce.number().optional(),
normalize: booleanCoercion.optional(), normalize: booleanCoercion.optional(),
continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(), continent: z.enum(GeoContinents).optional(),
}); });
export const getMonitorsByTeamIdParamValidation = z.object({}); export const getMonitorsByTeamIdParamValidation = z.object({});
@@ -26,14 +25,14 @@ export const getMonitorsByTeamIdQueryValidation = z.object({
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])), z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])),
]) ])
.optional(), .optional(),
filter: z.union([z.string(), z.literal("")]).optional(), filter: z.union([z.string(), z.literal(""), z.null()]).optional(),
}); });
export const getMonitorsWithChecksQueryValidation = z.object({ export const getMonitorsWithChecksQueryValidation = z.object({
limit: z.coerce.number().int().min(1).max(100).optional(), limit: z.coerce.number().int().min(1).max(100).optional(),
page: z.coerce.number().int().min(0).optional(), page: z.coerce.number().int().min(0).optional(),
rowsPerPage: z.coerce.number().int().min(1).max(100).optional(), rowsPerPage: z.coerce.number().int().min(1).max(100).optional(),
filter: z.union([z.string(), z.literal("")]).optional(), filter: z.union([z.string(), z.literal(""), z.null()]).optional(),
field: z.string().optional(), field: z.string().optional(),
order: z.enum(["asc", "desc"]).optional(), order: z.enum(["asc", "desc"]).optional(),
type: z type: z
@@ -52,8 +51,8 @@ export const getCertificateParamValidation = z.object({
export const createMonitorBodyValidation = z.object({ export const createMonitorBodyValidation = z.object({
_id: z.string().optional(), _id: 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.null(), z.literal("")]).optional(),
type: z.enum(MonitorTypes, "Invalid monitor type"), type: z.string().min(1, "Type is required"),
statusWindowSize: z.number().min(1).max(20).default(5), statusWindowSize: z.number().min(1).max(20).default(5),
statusWindowThreshold: z.number().min(1).max(100).default(60), statusWindowThreshold: z.number().min(1).max(100).default(60),
url: z.string().min(1, "URL is required"), url: z.string().min(1, "URL is required"),
@@ -70,7 +69,7 @@ export const createMonitorBodyValidation = z.object({
secret: z.string().optional(), secret: z.string().optional(),
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.string(), z.null(), z.literal("")]).optional(),
gameId: z.union([z.string(), z.literal("")]).optional(), gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""), grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
selectedDisks: z.array(z.string()).optional(), selectedDisks: z.array(z.string()).optional(),
@@ -82,11 +81,11 @@ export const createMonitorBodyValidation = z.object({
export const editMonitorBodyValidation = z.object({ export const editMonitorBodyValidation = z.object({
name: z.string().optional(), name: z.string().optional(),
type: z.enum(MonitorTypes).optional(), type: z.string().optional(),
url: z.string().optional(), url: z.string().optional(),
statusWindowSize: z.number().min(1).max(20).default(5), statusWindowSize: z.number().min(1).max(20).default(5),
statusWindowThreshold: z.number().min(1).max(100).default(60), statusWindowThreshold: z.number().min(1).max(100).default(60),
description: z.union([z.string(), z.literal("")]).optional(), description: z.union([z.string(), z.null(), z.literal("")]).optional(),
interval: z.number().optional(), interval: z.number().optional(),
notifications: z.array(z.string()).optional(), notifications: z.array(z.string()).optional(),
secret: z.string().optional(), secret: z.string().optional(),
@@ -94,7 +93,7 @@ export const editMonitorBodyValidation = z.object({
useAdvancedMatching: z.boolean().optional(), useAdvancedMatching: z.boolean().optional(),
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.string(), z.null(), z.literal("")]).optional(),
port: z.number().min(1).max(65535).optional(), port: z.number().min(1).max(65535).optional(),
cpuAlertThreshold: z.number().optional(), cpuAlertThreshold: z.number().optional(),
memoryAlertThreshold: z.number().optional(), memoryAlertThreshold: z.number().optional(),
@@ -113,63 +112,6 @@ export const pauseMonitorParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"), monitorId: z.string().min(1, "Monitor ID is required"),
}); });
export const getUptimeDetailsByIdParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
export const getUptimeDetailsByIdQueryValidation = z.object({
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
normalize: booleanCoercion.optional(),
});
const importedMonitorSchema = z.object({
id: z.string().optional(),
userId: z.string().optional(),
teamId: z.string().optional(),
name: z.string().min(1, "Name is required"),
description: z.union([z.string(), z.literal("")]).optional(),
status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).default("initializing"),
statusWindow: z.array(z.boolean()).default([]),
statusWindowSize: z.number().min(1).max(20).default(5),
statusWindowThreshold: z.number().min(1).max(100).default(60),
type: z.enum(MonitorTypes, "Invalid monitor type"),
ignoreTlsErrors: z.boolean().default(false),
useAdvancedMatching: z.boolean().default(false),
jsonPath: z.union([z.string(), z.literal("")]).optional(),
expectedValue: z.union([z.string(), z.literal("")]).optional(),
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
url: z.string().min(1, "URL is required"),
port: z.number().optional(),
isActive: z.boolean().default(true),
interval: z.number().default(60000),
uptimePercentage: z.number().optional(),
notifications: z.array(z.string()).default([]),
secret: z.string().optional(),
cpuAlertThreshold: z.number().default(100),
cpuAlertCounter: z.number().default(5),
memoryAlertThreshold: z.number().default(100),
memoryAlertCounter: z.number().default(5),
diskAlertThreshold: z.number().default(100),
diskAlertCounter: z.number().default(5),
tempAlertThreshold: z.number().default(100),
tempAlertCounter: z.number().default(5),
selectedDisks: z.array(z.string()).default([]),
gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
group: z.union([z.string().max(50).trim(), z.null()]).default(null),
geoCheckEnabled: z.boolean().default(false),
geoCheckLocations: z.array(z.enum(GeoContinents)).default([]),
geoCheckInterval: z.number().min(300000).default(300000),
createdAt: z.string().optional(),
updatedAt: z.string().optional(),
});
export const importMonitorsBodyValidation = z.object({
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"),
}); });
@@ -67,22 +67,6 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [
}), }),
]); ]);
export const testNotificationBodyValidation = createNotificationBodyValidation;
export const deleteNotificationParamValidation = z.object({
id: z.string().min(1, "Notification ID is required"),
});
export const getNotificationByIdParamValidation = z.object({
id: z.string().min(1, "Notification ID is required"),
});
export const editNotificationParamValidation = z.object({
id: z.string().min(1, "Notification ID is required"),
});
export const testAllNotificationsBodyValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
export const sendTestEmailBodyValidation = z.object({ export const sendTestEmailBodyValidation = z.object({
to: z.string().min(1, "To field is required"), to: z.string().min(1, "To field is required"),
systemEmailHost: z.string().optional(), systemEmailHost: z.string().optional(),
@@ -98,20 +82,3 @@ 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"],
}
);
+15 -15
View File
@@ -7,17 +7,17 @@ import { z } from "zod";
export const updateAppSettingsBodyValidation = z export const updateAppSettingsBodyValidation = z
.object({ .object({
checkTTL: z.number().int().min(1).max(CHECK_TTL_SENTINEL).optional(), checkTTL: z.union([z.number().int().min(1).max(CHECK_TTL_SENTINEL), z.literal("")]).optional(),
systemEmailPort: z.number().nullable().optional(), systemEmailPort: z.union([z.number(), z.literal("")]).optional(),
pagespeedApiKey: z.string().nullable().optional(), pagespeedApiKey: z.union([z.string(), z.literal("")]).optional(),
language: z.string().optional(), language: z.union([z.string(), z.literal("")]).optional(),
timezone: z.string().optional(), timezone: z.union([z.string(), z.literal("")]).optional(),
systemEmailHost: z.string().nullable().optional(), systemEmailHost: z.union([z.string(), z.literal("")]).optional(),
systemEmailAddress: z.string().nullable().optional(), systemEmailAddress: z.union([z.string(), z.literal("")]).optional(),
systemEmailPassword: z.string().nullable().optional(), systemEmailPassword: z.union([z.string(), z.literal("")]).optional(),
systemEmailUser: z.string().nullable().optional(), systemEmailUser: z.union([z.string(), z.literal("")]).optional(),
systemEmailConnectionHost: z.string().nullable().optional(), systemEmailConnectionHost: z.union([z.string(), z.literal("")]).optional(),
systemEmailTLSServername: z.string().nullable().optional(), systemEmailTLSServername: z.union([z.string(), z.literal("")]).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.number().min(1).max(100).optional(), cpu: z.union([z.number().min(1).max(100), z.literal("")]).optional(),
memory: z.number().min(1).max(100).optional(), memory: z.union([z.number().min(1).max(100), z.literal("")]).optional(),
disk: z.number().min(1).max(100).optional(), disk: z.union([z.number().min(1).max(100), z.literal("")]).optional(),
temperature: z.number().min(1).max(150).optional(), temperature: z.union([z.number().min(1).max(150), z.literal("")]).optional(),
}) })
.optional(), .optional(),
}) })
@@ -1,6 +1,5 @@
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
@@ -11,13 +10,13 @@ export const getStatusPageParamValidation = z.object({
}); });
export const getStatusPageQueryValidation = z.object({ export const getStatusPageQueryValidation = z.object({
type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])), type: z.array(z.enum(["uptime", "infrastructure"])),
timeFrame: z.coerce.number().optional(), timeFrame: z.coerce.number().optional(),
}); });
export const createStatusPageBodyValidation = z export const createStatusPageBodyValidation = z
.object({ .object({
type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])), type: z.array(z.enum(["uptime", "infrastructure"])),
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",
@@ -31,7 +30,6 @@ 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();