Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76f550843b | ||
|
|
15d8a8ba1f | ||
|
|
e878f51887 | ||
|
|
16b267d3a5 | ||
|
|
c156a49cc0 | ||
|
|
20ff76df9d | ||
|
|
ff90953f17 | ||
|
|
545926cfcf | ||
|
|
42f6c42ae1 | ||
|
|
456237ba4e | ||
|
|
e887c2308a | ||
|
|
d07f259829 | ||
|
|
566011c4e9 | ||
|
|
cda0ec3a4c | ||
|
|
d00889ff9b | ||
|
|
eafd97b9ad |
No files matched your search
@@ -27,14 +27,12 @@ export const useStatusPageForm = ({
|
||||
companyName: data?.companyName || "",
|
||||
url: data?.url || generateDefaultUrl(),
|
||||
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
type: data?.type || ["uptime"],
|
||||
color: data?.color || "#4169E1",
|
||||
monitors: data?.monitors || [],
|
||||
isPublished: data?.isPublished ?? false,
|
||||
showCharts: data?.showCharts ?? true,
|
||||
showUptimePercentage: data?.showUptimePercentage ?? true,
|
||||
showAdminLoginLink: data?.showAdminLoginLink ?? false,
|
||||
showInfrastructure: data?.showInfrastructure ?? false,
|
||||
customCSS: data?.customCSS || "",
|
||||
logo: transformLogo(data?.logo),
|
||||
};
|
||||
|
||||
@@ -302,49 +302,49 @@ const CreateMonitorPage = () => {
|
||||
>
|
||||
<RadioWithDescription
|
||||
value="http"
|
||||
label={t("pages.common.monitors.monitorTypes.optionHttp")}
|
||||
label={t("pages.createMonitor.form.type.optionHttp")}
|
||||
description={t(
|
||||
"pages.createMonitor.form.type.optionHttpDescription"
|
||||
)}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="ping"
|
||||
label={t("pages.common.monitors.monitorTypes.optionPing")}
|
||||
label={t("pages.createMonitor.form.type.optionPing")}
|
||||
description={t(
|
||||
"pages.createMonitor.form.type.optionPingDescription"
|
||||
)}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="docker"
|
||||
label={t("pages.common.monitors.monitorTypes.optionDocker")}
|
||||
label={t("pages.createMonitor.form.type.optionDocker")}
|
||||
description={t(
|
||||
"pages.createMonitor.form.type.optionDockerDescription"
|
||||
)}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="port"
|
||||
label={t("pages.common.monitors.monitorTypes.optionPort")}
|
||||
label={t("pages.createMonitor.form.type.optionPort")}
|
||||
description={t(
|
||||
"pages.createMonitor.form.type.optionPortDescription"
|
||||
)}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="game"
|
||||
label={t("pages.common.monitors.monitorTypes.optionGame")}
|
||||
label={t("pages.createMonitor.form.type.optionGame")}
|
||||
description={t(
|
||||
"pages.createMonitor.form.type.optionGameDescription"
|
||||
)}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="grpc"
|
||||
label={t("pages.common.monitors.monitorTypes.optionGrpc")}
|
||||
label={t("pages.createMonitor.form.type.optionGrpc")}
|
||||
description={t(
|
||||
"pages.createMonitor.form.type.optionGrpcDescription"
|
||||
)}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="websocket"
|
||||
label={t("pages.common.monitors.monitorTypes.optionWebSocket")}
|
||||
label={t("pages.createMonitor.form.type.optionWebSocket")}
|
||||
description={t(
|
||||
"pages.createMonitor.form.type.optionWebSocketDescription"
|
||||
)}
|
||||
|
||||
@@ -21,14 +21,14 @@ import {
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
|
||||
import type { StatusPageFormData } from "@/Validation/statusPage";
|
||||
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import type { MonitorDisplayType, StatusPageResponse } from "@/Types/StatusPage";
|
||||
import type { StatusPageResponse } from "@/Types/StatusPage";
|
||||
import timezones from "@/Utils/timezones.json";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
@@ -39,22 +39,6 @@ interface TimezoneOption {
|
||||
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 theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -65,12 +49,10 @@ const CreateStatusPage = () => {
|
||||
|
||||
// Fetch existing status page data when configuring
|
||||
const { data: statusPageData, isLoading: isLoadingStatusPage } =
|
||||
useGet<StatusPageResponse>(
|
||||
isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure`
|
||||
);
|
||||
useGet<StatusPageResponse>(isCreate ? null : `/status-page/${url}?type=uptime`);
|
||||
|
||||
const { data: monitorsResponse } = useGet<Monitor[]>(
|
||||
"/monitors/team?type=http&type=ping&type=port&type=docker&type=hardware"
|
||||
"/monitors/team?type=http&type=ping&type=port&type=docker"
|
||||
);
|
||||
const monitors = monitorsResponse ?? [];
|
||||
|
||||
@@ -99,24 +81,6 @@ const CreateStatusPage = () => {
|
||||
reset(defaults);
|
||||
}, [defaults, reset]);
|
||||
|
||||
const watchedMonitorIds: string[] = form.watch("monitors") ?? [];
|
||||
const computedTypes: MonitorDisplayType[] = useMemo(() => {
|
||||
const selectedMonitors = (watchedMonitorIds ?? [])
|
||||
.map((id) => monitors.find((m) => m.id === id))
|
||||
.filter((m): m is Monitor => m !== undefined);
|
||||
|
||||
const typesSet = new Set<MonitorDisplayType>();
|
||||
selectedMonitors.forEach((m) => {
|
||||
typesSet.add(m.type === "hardware" ? "infrastructure" : "uptime");
|
||||
});
|
||||
|
||||
return typesSet.size ? Array.from(typesSet) : ["uptime"];
|
||||
}, [JSON.stringify(watchedMonitorIds), monitors]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue("type", computedTypes);
|
||||
}, [computedTypes]);
|
||||
|
||||
const onError = (errors: any) => {
|
||||
logger.debug("Status page validation errors", errors);
|
||||
};
|
||||
@@ -139,6 +103,7 @@ const CreateStatusPage = () => {
|
||||
|
||||
const onSubmit = async (data: StatusPageFormData) => {
|
||||
const fd = new FormData();
|
||||
fd.append("type", "uptime");
|
||||
fd.append("isPublished", String(data.isPublished));
|
||||
if (data.companyName) fd.append("companyName", data.companyName);
|
||||
if (data.url) fd.append("url", data.url);
|
||||
@@ -147,16 +112,11 @@ const CreateStatusPage = () => {
|
||||
fd.append("showCharts", String(data.showCharts));
|
||||
fd.append("showUptimePercentage", String(data.showUptimePercentage));
|
||||
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
|
||||
fd.append("showInfrastructure", String(data.showInfrastructure));
|
||||
|
||||
data.monitors.forEach((monitorId) => {
|
||||
fd.append("monitors[]", monitorId);
|
||||
});
|
||||
|
||||
data.type.forEach((type) => {
|
||||
fd.append("type[]", type);
|
||||
});
|
||||
|
||||
// Handle logo upload
|
||||
if (data.logo === null) {
|
||||
// Signal to remove the logo
|
||||
@@ -343,9 +303,7 @@ const CreateStatusPage = () => {
|
||||
}}
|
||||
>
|
||||
<GripVertical size={20} />
|
||||
<Typography
|
||||
flexGrow={1}
|
||||
>{`${monitor.name} (${getMonitorTypeLabel(monitor.type, t)})`}</Typography>
|
||||
<Typography flexGrow={1}>{monitor.name}</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
@@ -467,23 +425,6 @@ const CreateStatusPage = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="showInfrastructure"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
}
|
||||
label={t(
|
||||
"pages.statusPages.form.features.option.showInfrastructure.label"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{/* <Controller
|
||||
name="showUptimePercentage"
|
||||
control={control}
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Gauge } from "@/Components/design-elements";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import Box from "@mui/material/Box";
|
||||
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||
|
||||
const GAUGE_RADIUS = 60;
|
||||
const GAUGE_STROKE_WIDTH = 12;
|
||||
const PERCENTAGE_MULTIPLIER = 100;
|
||||
|
||||
interface StatusPageMonitor extends Monitor {
|
||||
checks?: Monitor["recentChecks"];
|
||||
}
|
||||
|
||||
interface MetricDetail {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface MetricConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
hasData: boolean;
|
||||
progress: number;
|
||||
details: MetricDetail[];
|
||||
}
|
||||
|
||||
const MetricDetailRow = ({ label, value }: MetricDetail) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant="body2">{value}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
interface MetricItemProps {
|
||||
label: string;
|
||||
progress: number;
|
||||
details?: MetricDetail[];
|
||||
}
|
||||
|
||||
const MetricItem = ({ label, progress, details }: MetricItemProps) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
return (
|
||||
<Grid
|
||||
size={isSmall ? 12 : 4}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
gap: theme.spacing(SPACING.LG),
|
||||
padding: theme.spacing(LAYOUT.XS),
|
||||
borderRight: isSmall ? "none" : `1px solid ${theme.palette.divider}`,
|
||||
borderBottom: isSmall ? `1px solid ${theme.palette.divider}` : "none",
|
||||
"&:last-child": {
|
||||
borderRight: "none",
|
||||
borderBottom: "none",
|
||||
paddingBottom: theme.spacing(SPACING.LG),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
>
|
||||
<Gauge
|
||||
progress={progress}
|
||||
radius={GAUGE_RADIUS}
|
||||
strokeWidth={GAUGE_STROKE_WIDTH}
|
||||
/>
|
||||
<Typography variant="body2">{label}</Typography>
|
||||
</Box>
|
||||
{details && details.length > 0 && (
|
||||
<Box
|
||||
width="100%"
|
||||
paddingX={theme.spacing(LAYOUT.LG)}
|
||||
>
|
||||
{details.map((detail) => (
|
||||
<MetricDetailRow
|
||||
key={detail.label}
|
||||
label={detail.label}
|
||||
value={detail.value}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
type LatestCheck = NonNullable<Monitor["recentChecks"]>[number];
|
||||
|
||||
const buildCpuMetric = (
|
||||
check: LatestCheck,
|
||||
t: (key: string) => string
|
||||
): MetricConfig | null => {
|
||||
if (!check.cpu || typeof check.cpu.usage_percent !== "number") {
|
||||
return null;
|
||||
}
|
||||
const usagePercent = (check.cpu.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER;
|
||||
return {
|
||||
key: "cpu",
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.cpu"),
|
||||
hasData: true,
|
||||
progress: usagePercent,
|
||||
details: [
|
||||
{
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.usage"),
|
||||
value: `${usagePercent.toFixed(2)}%`,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const buildMemoryMetric = (
|
||||
check: LatestCheck,
|
||||
t: (key: string) => string
|
||||
): MetricConfig | null => {
|
||||
if (
|
||||
!check.memory ||
|
||||
typeof check.memory.usage_percent !== "number" ||
|
||||
typeof check.memory.used_bytes !== "number" ||
|
||||
typeof check.memory.total_bytes !== "number"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: "memory",
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.memory"),
|
||||
hasData: true,
|
||||
progress: (check.memory.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER,
|
||||
details: [
|
||||
{
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.used"),
|
||||
value: prettyBytes(check.memory.used_bytes ?? 0),
|
||||
},
|
||||
{
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.total"),
|
||||
value: prettyBytes(check.memory.total_bytes ?? 0),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const buildDiskMetric = (
|
||||
check: LatestCheck,
|
||||
t: (key: string) => string
|
||||
): MetricConfig | null => {
|
||||
if (!check.disk || check.disk.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const disks = check.disk;
|
||||
const avgUsagePercent =
|
||||
(disks.reduce((acc, disk) => acc + (disk?.usage_percent ?? 0), 0) / disks.length) *
|
||||
PERCENTAGE_MULTIPLIER;
|
||||
const totalUsedBytes = disks.reduce((acc, disk) => acc + (disk?.used_bytes ?? 0), 0);
|
||||
const totalTotalBytes = disks.reduce((acc, disk) => acc + (disk?.total_bytes ?? 0), 0);
|
||||
|
||||
return {
|
||||
key: "disk",
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.disk"),
|
||||
hasData: true,
|
||||
progress: avgUsagePercent,
|
||||
details: [
|
||||
{
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.used"),
|
||||
value: prettyBytes(totalUsedBytes),
|
||||
},
|
||||
{
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.total"),
|
||||
value: prettyBytes(totalTotalBytes),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0];
|
||||
|
||||
if (!latestCheck) {
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{t("pages.statusPages.monitorsList.noData")}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const metrics: MetricConfig[] = [
|
||||
buildCpuMetric(latestCheck, t),
|
||||
buildMemoryMetric(latestCheck, t),
|
||||
buildDiskMetric(latestCheck, t),
|
||||
].filter((m): m is MetricConfig => m !== null);
|
||||
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{t("pages.statusPages.monitorsList.noData")}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
alignItems="center"
|
||||
padding={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
{metrics.map(({ key, label, progress, details }) => (
|
||||
<MetricItem
|
||||
key={key}
|
||||
label={label}
|
||||
progress={progress}
|
||||
details={details}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -5,15 +5,13 @@ import Typography from "@mui/material/Typography";
|
||||
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common";
|
||||
import { StatusLabel, BaseBox } from "@/Components/design-elements";
|
||||
import { SwitchComponent } from "@/Components/inputs";
|
||||
import { InfrastructureMetrics } from "@/Pages/StatusPage/Status/Components/InfrastructureMetrics";
|
||||
|
||||
import { useTheme, type Theme } from "@mui/material/styles";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState } from "react";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||
|
||||
interface StatusPageMonitor extends Monitor {
|
||||
checks?: Monitor["recentChecks"];
|
||||
@@ -24,87 +22,6 @@ interface MonitorsListProps {
|
||||
monitors: StatusPageMonitor[];
|
||||
}
|
||||
|
||||
const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => {
|
||||
// for future: specific badge styles can be defined here based on monitor type
|
||||
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 {
|
||||
backgroundColor: config.bg,
|
||||
color: config.color,
|
||||
padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
};
|
||||
};
|
||||
|
||||
// Extracted component for monitor content below the header row
|
||||
const MonitorContent = ({
|
||||
monitor,
|
||||
statusPage,
|
||||
chartType,
|
||||
}: {
|
||||
monitor: Monitor & { checks?: Monitor["recentChecks"] };
|
||||
statusPage: StatusPage;
|
||||
chartType: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isInfrastructureMonitor = monitor?.type === "hardware";
|
||||
|
||||
if (isInfrastructureMonitor) {
|
||||
if (statusPage.showInfrastructure === false) {
|
||||
return null;
|
||||
}
|
||||
return <InfrastructureMetrics monitor={monitor} />;
|
||||
}
|
||||
|
||||
if (statusPage.showCharts === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1, mb: theme.spacing(SPACING.LG) }}>
|
||||
{chartType === "histogram" ? (
|
||||
<HistogramResponseTime
|
||||
height={{ xs: 50, md: 100 }}
|
||||
gap={{ xs: theme.spacing(SPACING.SM), md: theme.spacing(LAYOUT.SM) }}
|
||||
checks={monitor?.checks?.slice().reverse() ?? []}
|
||||
/>
|
||||
) : (
|
||||
<HeatmapResponseTime checks={monitor?.checks?.slice().reverse() ?? []} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -114,12 +31,11 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||
{statusPage.showCharts && (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
{statusPage.showCharts !== false && (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
>
|
||||
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
|
||||
<SwitchComponent
|
||||
@@ -140,62 +56,52 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||
return (
|
||||
<BaseBox
|
||||
key={monitor.id}
|
||||
padding={theme.spacing(LAYOUT.MD)}
|
||||
padding={theme.spacing(4)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
mb={theme.spacing(LAYOUT.XS)}
|
||||
gap={theme.spacing(4)}
|
||||
mb={statusPage.showCharts !== false ? theme.spacing(4) : 0}
|
||||
>
|
||||
<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",
|
||||
}}
|
||||
>
|
||||
<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 && (
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
{showURL && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.text.secondary}
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
color="text.secondary"
|
||||
>
|
||||
{monitor.url}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<StatusLabel status={monitor?.status} />
|
||||
<StatusLabel status={monitor.status} />
|
||||
</Stack>
|
||||
<MonitorContent
|
||||
monitor={monitor}
|
||||
statusPage={statusPage}
|
||||
chartType={chartType}
|
||||
/>
|
||||
{statusPage.showCharts !== false && (
|
||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
||||
{chartType === "histogram" ? (
|
||||
<HistogramResponseTime
|
||||
height={{ xs: 50, md: 100 }}
|
||||
gap={{ xs: theme.spacing(0.5), md: theme.spacing(5) }}
|
||||
checks={monitor?.checks?.slice().reverse() ?? []}
|
||||
/>
|
||||
) : (
|
||||
<HeatmapResponseTime
|
||||
checks={monitor?.checks?.slice().reverse() ?? []}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</BaseBox>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -23,17 +23,16 @@ const StatusPageView = () => {
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const isPublic = location.pathname.startsWith("/status/public");
|
||||
|
||||
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
|
||||
const apiUrl = url ? `/status-page/${url}?type=uptime` : null;
|
||||
|
||||
const { data, isLoading, error } = useGet<StatusPageResponse>(apiUrl);
|
||||
|
||||
const statusPage = data?.statusPage;
|
||||
const monitors = data?.monitors ?? [];
|
||||
const infrastructureMonitors = data?.infrastructureMonitors ?? [];
|
||||
|
||||
if (!statusPage) return null;
|
||||
|
||||
if (monitors?.length === 0 && infrastructureMonitors?.length === 0) {
|
||||
if (monitors?.length === 0) {
|
||||
return (
|
||||
<BasePage
|
||||
loading={isLoading}
|
||||
|
||||
@@ -97,7 +97,7 @@ export const StatusPagesTable = ({
|
||||
{
|
||||
id: "type",
|
||||
content: t("common.table.headers.type"),
|
||||
render: (row) => row.type.join(", "),
|
||||
render: (row) => row.type,
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
export type MonitorDisplayType = "uptime" | "infrastructure";
|
||||
|
||||
export interface StatusPage {
|
||||
id: string;
|
||||
userId: string;
|
||||
teamId: string;
|
||||
type: MonitorDisplayType[];
|
||||
type: string;
|
||||
companyName: string;
|
||||
url: string;
|
||||
timezone?: string;
|
||||
@@ -21,7 +20,6 @@ export interface StatusPage {
|
||||
showCharts: boolean;
|
||||
showUptimePercentage: boolean;
|
||||
showAdminLoginLink: boolean;
|
||||
showInfrastructure: boolean;
|
||||
customCSS: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -30,5 +28,4 @@ export interface StatusPage {
|
||||
export interface StatusPageResponse {
|
||||
statusPage: StatusPage;
|
||||
monitors: Monitor[];
|
||||
infrastructureMonitors?: Monitor[];
|
||||
}
|
||||
@@ -14,16 +14,12 @@ export const statusPageSchema = z.object({
|
||||
"URL can only contain lowercase letters, numbers, and hyphens"
|
||||
),
|
||||
timezone: z.string().optional(),
|
||||
type: z
|
||||
.array(z.enum(["uptime", "infrastructure"]))
|
||||
.min(1, "At least one type is required"),
|
||||
color: z.string().min(1, "Color is required"),
|
||||
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
|
||||
isPublished: z.boolean(),
|
||||
showCharts: z.boolean(),
|
||||
showUptimePercentage: z.boolean(),
|
||||
showAdminLoginLink: z.boolean(),
|
||||
showInfrastructure: z.boolean(),
|
||||
customCSS: z.string().optional(),
|
||||
logo: z
|
||||
.object({
|
||||
|
||||
@@ -288,13 +288,7 @@ const logoImageValidation = joi
|
||||
.optional(); // Make entire object optional
|
||||
|
||||
const statusPageValidation = joi.object({
|
||||
type: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.string().valid("uptime", "infrastructure"),
|
||||
joi.array().items(joi.string().valid("uptime", "infrastructure"))
|
||||
)
|
||||
.required(),
|
||||
type: joi.string().valid("uptime").required(),
|
||||
isPublished: joi.bool(),
|
||||
companyName: joi
|
||||
.string()
|
||||
@@ -323,7 +317,6 @@ const statusPageValidation = joi.object({
|
||||
showUptimePercentage: joi.boolean(),
|
||||
showCharts: joi.boolean(),
|
||||
showAdminLoginLink: joi.boolean(),
|
||||
showInfrastructure: joi.boolean(),
|
||||
});
|
||||
|
||||
const settingsValidation = joi.object({
|
||||
|
||||
@@ -407,17 +407,6 @@
|
||||
"paused": "paused",
|
||||
"total": "total",
|
||||
"up": "up"
|
||||
},
|
||||
"monitorTypes": {
|
||||
"optionDocker": "Docker",
|
||||
"optionGame": "Game",
|
||||
"optionHttp": "HTTP(S)",
|
||||
"optionPing": "Ping",
|
||||
"optionPort": "Port",
|
||||
"optionPagespeed": "PageSpeed",
|
||||
"optionHardware": "Infrastructure",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -545,12 +534,19 @@
|
||||
},
|
||||
"type": {
|
||||
"description": "Select the type of check to perform",
|
||||
"optionDocker": "Docker",
|
||||
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
||||
"optionGame": "Game",
|
||||
"optionGameDescription": "Monitor if a specific game server is online.",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
||||
"optionWebSocket": "WebSocket",
|
||||
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
||||
"optionHttp": "HTTP(S)",
|
||||
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
||||
"optionPing": "Ping",
|
||||
"optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
|
||||
"optionPort": "Port",
|
||||
"optionPortDescription": "Monitor if a specific port on a server is open.",
|
||||
"title": "Type"
|
||||
},
|
||||
@@ -1182,22 +1178,7 @@
|
||||
},
|
||||
"monitorsList": {
|
||||
"chartTypeHeatmap": "Heatmap",
|
||||
"chartTypeHistogram": "Histogram",
|
||||
"noData": "No data available",
|
||||
"infrastructure": {
|
||||
"title": "Infrastructure",
|
||||
"cpu": "CPU",
|
||||
"memory": "RAM",
|
||||
"memoryText": "Memory",
|
||||
"disk": "Disk",
|
||||
"usage": "Usage",
|
||||
"used": "Used",
|
||||
"total": "Total"
|
||||
},
|
||||
"uptime": {
|
||||
"title": "Uptime",
|
||||
"responseTime": "Response time"
|
||||
}
|
||||
"chartTypeHistogram": "Histogram"
|
||||
},
|
||||
"statusBar": {
|
||||
"allDown": "All systems are down",
|
||||
@@ -1289,9 +1270,6 @@
|
||||
},
|
||||
"showAdminLoginLink": {
|
||||
"label": "Show admin login link"
|
||||
},
|
||||
"showInfrastructure": {
|
||||
"label": "Show infrastructure metrics"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index
|
||||
import sslChecker, { SSLDetails } from "ssl-checker";
|
||||
type SSLCheckerType = typeof sslChecker;
|
||||
|
||||
const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise<SSLDetails> => {
|
||||
export const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise<SSLDetails> => {
|
||||
const monitorUrl = new URL(monitor.url);
|
||||
const hostname = monitorUrl.hostname;
|
||||
const cert = await checker(hostname);
|
||||
@@ -12,14 +12,14 @@ const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor
|
||||
}
|
||||
return cert;
|
||||
};
|
||||
const requireString = (value: unknown, fieldName: string): string => {
|
||||
export const requireString = (value: unknown, fieldName: string): string => {
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
return value;
|
||||
}
|
||||
throw new AppError({ message: `${fieldName} is required`, status: 400 });
|
||||
};
|
||||
|
||||
const optionalString = (value: unknown, fieldName: string): string | undefined => {
|
||||
export const optionalString = (value: unknown, fieldName: string): string | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ const optionalString = (value: unknown, fieldName: string): string | undefined =
|
||||
throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
|
||||
};
|
||||
|
||||
const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
|
||||
export const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ const optionalNumber = (value: unknown, fieldName: string): number | undefined =
|
||||
throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
|
||||
};
|
||||
|
||||
const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
|
||||
export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -63,7 +63,7 @@ const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined
|
||||
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
|
||||
};
|
||||
|
||||
const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
|
||||
export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
|
||||
const parseSingle = (input: unknown): MonitorType => {
|
||||
if (typeof input !== "string") {
|
||||
throw new AppError({ message: "Monitor type must be a string", status: 400 });
|
||||
@@ -83,7 +83,7 @@ const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | u
|
||||
return parseSingle(value);
|
||||
};
|
||||
|
||||
const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
||||
export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -93,42 +93,36 @@ const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
||||
throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 });
|
||||
};
|
||||
|
||||
const requireTeamId = (teamId?: string): string => {
|
||||
export const requireTeamId = (teamId?: string): string => {
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
return teamId;
|
||||
};
|
||||
|
||||
const requireUserId = (userId?: string): string => {
|
||||
export const requireUserId = (userId?: string): string => {
|
||||
if (!userId) {
|
||||
throw new AppError({ message: "User ID is required", status: 400 });
|
||||
}
|
||||
return userId;
|
||||
};
|
||||
const requireUserEmail = (userEmail?: string): string => {
|
||||
export const requireUserEmail = (userEmail?: string): string => {
|
||||
if (!userEmail) {
|
||||
throw new AppError({ message: "User email is required", status: 400 });
|
||||
}
|
||||
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[] => {
|
||||
if (!userRoles || userRoles.length === 0) {
|
||||
throw new AppError({ message: "User roles are required", status: 400 });
|
||||
}
|
||||
return userRoles;
|
||||
};
|
||||
|
||||
export {
|
||||
fetchMonitorCertificate,
|
||||
requireString,
|
||||
optionalString,
|
||||
optionalNumber,
|
||||
optionalBoolean,
|
||||
parseMonitorTypeFilter,
|
||||
parseSortOrder,
|
||||
requireTeamId,
|
||||
requireUserId,
|
||||
requireUserEmail,
|
||||
};
|
||||
@@ -1,35 +1,30 @@
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js";
|
||||
import { IIncidentService } from "@/service/index.js";
|
||||
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
|
||||
|
||||
const SERVICE_NAME = "incidentController";
|
||||
|
||||
const SERVICE_NAME = "IncidentController";
|
||||
class IncidentController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private incidentService: any;
|
||||
constructor(incidentService: any) {
|
||||
private incidentService: IIncidentService;
|
||||
constructor(incidentService: IIncidentService) {
|
||||
this.incidentService = incidentService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return IncidentController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {};
|
||||
const validatedQuery = getIncidentsByTeamQueryValidation.parse(req.query);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const result = await this.incidentService.getIncidentsByTeam(
|
||||
teamId,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
page,
|
||||
rowsPerPage,
|
||||
status,
|
||||
monitorId,
|
||||
resolutionType
|
||||
validatedQuery.sortOrder,
|
||||
validatedQuery.dateRange,
|
||||
validatedQuery.page,
|
||||
validatedQuery.rowsPerPage,
|
||||
validatedQuery.status,
|
||||
validatedQuery.monitorId,
|
||||
validatedQuery.resolutionType
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -45,10 +40,9 @@ class IncidentController {
|
||||
getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const validatedQuery = getIncidentSummaryQueryValidation.parse(req.query);
|
||||
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10;
|
||||
|
||||
const summary = await this.incidentService.getIncidentSummary(teamId, limit);
|
||||
const summary = await this.incidentService.getIncidentSummary(teamId, validatedQuery.limit);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Incident summary retrieved successfully",
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js";
|
||||
import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
||||
import { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
||||
import { IInviteService } from "@/service/index.js";
|
||||
const SERVICE_NAME = "inviteController";
|
||||
|
||||
class InviteController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private inviteService: any;
|
||||
constructor(inviteService: any) {
|
||||
private inviteService: IInviteService;
|
||||
constructor(inviteService: IInviteService) {
|
||||
this.inviteService = inviteService;
|
||||
}
|
||||
|
||||
@@ -34,8 +35,9 @@ class InviteController {
|
||||
|
||||
sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const userRoles = requireUserRoles(req?.user?.role);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const userRoles = requireUserRoles(req.user?.role);
|
||||
const firstName = requireFirstName(req.user?.firstName);
|
||||
|
||||
const inviteRequest = req.body;
|
||||
inviteRequest.teamId = teamId;
|
||||
@@ -43,7 +45,7 @@ class InviteController {
|
||||
|
||||
const inviteToken = await this.inviteService.sendInviteEmail({
|
||||
invite: inviteRequest,
|
||||
firstName: req?.user?.firstName,
|
||||
firstName,
|
||||
userRoles,
|
||||
});
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
const SERVICE_NAME = "LogController";
|
||||
|
||||
class LogController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private logger: any;
|
||||
constructor(logger: any) {
|
||||
private logger: ILogger;
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
get serviceName() {
|
||||
@@ -14,7 +15,7 @@ class LogController {
|
||||
|
||||
getLogs = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const logs = await this.logger.getLogs();
|
||||
const logs = this.logger.getLogs();
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
msg: "Logs fetched successfully",
|
||||
|
||||
@@ -9,26 +9,19 @@ import {
|
||||
deleteMaintenanceWindowByIdParamValidation,
|
||||
} from "@/validation/maintenanceWindowValidation.js";
|
||||
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
||||
|
||||
const SERVICE_NAME = "maintenanceWindowController";
|
||||
import { IMaintenanceWindowService } from "@/service/index.js";
|
||||
|
||||
class MaintenanceWindowController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private maintenanceWindowService: any;
|
||||
constructor(maintenanceWindowService: any) {
|
||||
private maintenanceWindowService: IMaintenanceWindowService;
|
||||
constructor(maintenanceWindowService: IMaintenanceWindowService) {
|
||||
this.maintenanceWindowService = maintenanceWindowService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return MaintenanceWindowController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
createMaintenanceWindowBodyValidation.parse(req.body);
|
||||
const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
|
||||
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: validatedBody });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -40,11 +33,11 @@ class MaintenanceWindowController {
|
||||
};
|
||||
getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
const validatedParams = getMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id as string, teamId });
|
||||
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: validatedParams.id, teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -58,11 +51,18 @@ class MaintenanceWindowController {
|
||||
|
||||
getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query);
|
||||
const validatedQuery = getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query);
|
||||
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query });
|
||||
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({
|
||||
teamId,
|
||||
active: validatedQuery.active,
|
||||
page: validatedQuery.page,
|
||||
rowsPerPage: validatedQuery.rowsPerPage,
|
||||
field: validatedQuery.field,
|
||||
order: validatedQuery.order,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -76,12 +76,12 @@ class MaintenanceWindowController {
|
||||
|
||||
getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params);
|
||||
const validatedParams = getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params);
|
||||
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({
|
||||
monitorId: req.params.monitorId as string,
|
||||
monitorId: validatedParams.monitorId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
@@ -96,11 +96,11 @@ class MaintenanceWindowController {
|
||||
};
|
||||
deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
deleteMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
const validatedParams = deleteMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id as string, teamId });
|
||||
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: validatedParams.id, teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -113,14 +113,14 @@ class MaintenanceWindowController {
|
||||
|
||||
editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
editMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
editMaintenanceByIdWindowBodyValidation.parse(req.body);
|
||||
const validatedParams = editMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
const validatedBody = editMaintenanceByIdWindowBodyValidation.parse(req.body);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({
|
||||
id: req.params.id as string,
|
||||
body: req.body,
|
||||
id: validatedParams.id,
|
||||
body: validatedBody,
|
||||
teamId,
|
||||
});
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Request, Response, NextFunction } from "express";
|
||||
import {
|
||||
getMonitorByIdParamValidation,
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByTeamIdParamValidation,
|
||||
getMonitorsByTeamIdQueryValidation,
|
||||
getMonitorsWithChecksQueryValidation,
|
||||
createMonitorBodyValidation,
|
||||
@@ -12,22 +11,14 @@ import {
|
||||
getCertificateParamValidation,
|
||||
getHardwareDetailsByIdParamValidation,
|
||||
getHardwareDetailsByIdQueryValidation,
|
||||
getUptimeDetailsByIdParamValidation,
|
||||
getUptimeDetailsByIdQueryValidation,
|
||||
importMonitorsBodyValidation,
|
||||
} from "@/validation/monitorValidation.js";
|
||||
import sslChecker from "ssl-checker";
|
||||
import {
|
||||
fetchMonitorCertificate,
|
||||
requireString,
|
||||
optionalString,
|
||||
optionalNumber,
|
||||
optionalBoolean,
|
||||
parseMonitorTypeFilter,
|
||||
parseSortOrder,
|
||||
requireTeamId,
|
||||
requireUserId,
|
||||
} from "./controllerUtils.js";
|
||||
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "./controllerUtils.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorService } from "@/service/index.js";
|
||||
import { GeoContinent } from "@/types/geoCheck.js";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
class MonitorController {
|
||||
@@ -45,10 +36,9 @@ class MonitorController {
|
||||
|
||||
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getCertificateParamValidation.parse(req.params);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const monitorId = requireString(req.params?.monitorId, "Monitor ID");
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||
const validatedParams = getCertificateParamValidation.parse(req.params);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId });
|
||||
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -65,10 +55,14 @@ class MonitorController {
|
||||
|
||||
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const normalize = optionalBoolean(req?.query?.normalize, "normalize");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params);
|
||||
const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query);
|
||||
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const dateRange = validatedQuery.dateRange;
|
||||
const normalize = validatedQuery.normalize;
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const data = await this.monitorService.getUptimeDetailsById({
|
||||
teamId,
|
||||
@@ -88,12 +82,12 @@ class MonitorController {
|
||||
|
||||
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||
getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent";
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const dateRange = validatedQuery.dateRange || "recent";
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const data = await this.monitorService.getHardwareDetailsById({
|
||||
teamId,
|
||||
@@ -112,12 +106,12 @@ class MonitorController {
|
||||
};
|
||||
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||
getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const dateRange = validatedQuery.dateRange || "recent";
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const data = await this.monitorService.getPageSpeedDetailsById({
|
||||
teamId,
|
||||
@@ -137,18 +131,14 @@ class MonitorController {
|
||||
|
||||
getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getMonitorByIdParamValidation.parse(req.params);
|
||||
getMonitorByIdQueryValidation.parse(req.query);
|
||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||
const validatedQuery = getMonitorByIdQueryValidation.parse(req.query);
|
||||
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const continentParam = req?.query?.continent;
|
||||
const continents = continentParam
|
||||
? Array.isArray(continentParam)
|
||||
? (continentParam as GeoContinent[])
|
||||
: [continentParam as GeoContinent]
|
||||
: undefined;
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const dateRange = validatedQuery.dateRange || "recent";
|
||||
const continentParam = validatedQuery.continent;
|
||||
const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined;
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const data = await this.monitorService.getGeoChecksByMonitorId({
|
||||
teamId,
|
||||
@@ -169,11 +159,10 @@ class MonitorController {
|
||||
|
||||
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getMonitorByIdParamValidation.parse(req.params);
|
||||
getMonitorByIdQueryValidation.parse(req.query);
|
||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const monitorId = validatedParams.monitorId;
|
||||
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||
|
||||
@@ -189,12 +178,12 @@ class MonitorController {
|
||||
|
||||
createMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
createMonitorBodyValidation.parse(req.body);
|
||||
const validatedBody = createMonitorBodyValidation.parse(req.body);
|
||||
|
||||
const userId = requireString(req?.user?.id, "User ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const userId = requireUserId(req.user?.id);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
|
||||
const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -208,14 +197,10 @@ class MonitorController {
|
||||
|
||||
importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const userId = requireUserId(req?.user?.id);
|
||||
|
||||
const { monitors } = req.body;
|
||||
|
||||
if (!monitors || !Array.isArray(monitors)) {
|
||||
throw new AppError({ message: "Invalid request: monitors array is required", status: 400 });
|
||||
}
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const userId = requireUserId(req.user?.id);
|
||||
const validatedBody = importMonitorsBodyValidation.parse(req.body);
|
||||
const monitors = validatedBody.monitors;
|
||||
|
||||
const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors });
|
||||
|
||||
@@ -231,9 +216,9 @@ class MonitorController {
|
||||
|
||||
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getMonitorByIdParamValidation.parse(req.params);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const monitorId = validatedParams.monitorId;
|
||||
|
||||
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
|
||||
|
||||
@@ -249,8 +234,7 @@ class MonitorController {
|
||||
|
||||
deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -264,12 +248,12 @@ class MonitorController {
|
||||
|
||||
editMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getMonitorByIdParamValidation.parse(req.params);
|
||||
editMonitorBodyValidation.parse(req.body);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||
const validatedBody = editMonitorBodyValidation.parse(req.body);
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
|
||||
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -283,10 +267,10 @@ class MonitorController {
|
||||
|
||||
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
pauseMonitorParamValidation.parse(req.params);
|
||||
const validatedParams = pauseMonitorParamValidation.parse(req.params);
|
||||
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
|
||||
|
||||
@@ -302,8 +286,8 @@ class MonitorController {
|
||||
|
||||
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const id = requireString(req?.user?.id, "User ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const id = requireUserId(req.user?.id);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -316,32 +300,13 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { to } = req.body;
|
||||
if (!to || typeof to !== "string") {
|
||||
throw new AppError({ message: "Invalid 'to' email address", status: 400 });
|
||||
}
|
||||
|
||||
const messageId = await this.monitorService.sendTestEmail({ to });
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Test email sent successfully",
|
||||
data: { messageId },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getMonitorsByTeamIdParamValidation.parse(req.params);
|
||||
getMonitorsByTeamIdQueryValidation.parse(req.query);
|
||||
const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query);
|
||||
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const type = parseMonitorTypeFilter(req.query?.type);
|
||||
const filter = optionalString(req.query?.filter, "filter");
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const type = validatedQuery.type;
|
||||
const filter = validatedQuery.filter;
|
||||
|
||||
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
|
||||
|
||||
@@ -357,17 +322,17 @@ class MonitorController {
|
||||
|
||||
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getMonitorsByTeamIdParamValidation.parse(req.params);
|
||||
getMonitorsWithChecksQueryValidation.parse(req.query);
|
||||
const explain = optionalBoolean(req?.query?.explain, "explain");
|
||||
const limit = optionalNumber(req?.query?.limit, "limit");
|
||||
const page = optionalNumber(req?.query?.page, "page");
|
||||
const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage");
|
||||
const filter = optionalString(req?.query?.filter, "filter");
|
||||
const field = optionalString(req?.query?.field, "field");
|
||||
const order = parseSortOrder(req?.query?.order);
|
||||
const type = parseMonitorTypeFilter(req?.query?.type);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query);
|
||||
|
||||
const explain = validatedQuery.explain;
|
||||
const limit = validatedQuery.limit;
|
||||
const page = validatedQuery.page;
|
||||
const rowsPerPage = validatedQuery.rowsPerPage;
|
||||
const filter = validatedQuery.filter;
|
||||
const field = validatedQuery.field;
|
||||
const order = validatedQuery.order;
|
||||
const type = validatedQuery.type;
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
|
||||
teamId,
|
||||
@@ -392,11 +357,7 @@ class MonitorController {
|
||||
|
||||
exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const json = await this.monitorService.exportMonitorsToJSON({ teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -424,11 +385,7 @@ class MonitorController {
|
||||
|
||||
getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const groups = await this.monitorService.getGroupsByTeamId({ teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
import { Notification } from "@/types/index.js";
|
||||
import { createNotificationBodyValidation } from "@/validation/notificationValidation.js";
|
||||
import {
|
||||
createNotificationBodyValidation,
|
||||
deleteNotificationParamValidation,
|
||||
getNotificationByIdParamValidation,
|
||||
testNotificationBodyValidation,
|
||||
editNotificationParamValidation,
|
||||
testAllNotificationsBodyValidation,
|
||||
} from "@/validation/notificationValidation.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { INotificationsService } from "@/service/index.js";
|
||||
import { requireTeamId } from "./controllerUtils.js";
|
||||
import { requireTeamId, requireUserId } from "./controllerUtils.js";
|
||||
|
||||
const SERVICE_NAME = "NotificationController";
|
||||
|
||||
class NotificationController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private notificationsService: INotificationsService;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
|
||||
@@ -18,13 +23,9 @@ class NotificationController {
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return NotificationController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
testNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const notification: Notification = req.body;
|
||||
const notification = testNotificationBodyValidation.parse(req.body);
|
||||
const success = await this.notificationsService.sendTestNotification(notification);
|
||||
|
||||
if (!success) {
|
||||
@@ -34,6 +35,7 @@ class NotificationController {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Notification sent successfully",
|
||||
details: { service: SERVICE_NAME },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -42,23 +44,12 @@ class NotificationController {
|
||||
|
||||
createNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
createNotificationBodyValidation.parse(req.body);
|
||||
const validatedBody = createNotificationBodyValidation.parse(req.body);
|
||||
|
||||
const body = req.body;
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const userId = requireUserId(req.user?.id);
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
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);
|
||||
const notification = await this.notificationsService.createNotification(validatedBody, userId, teamId);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Notification created successfully",
|
||||
@@ -71,11 +62,7 @@ class NotificationController {
|
||||
|
||||
getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const notifications = await this.notificationsService.findNotificationsByTeamId(teamId);
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -90,17 +77,10 @@ class NotificationController {
|
||||
|
||||
deleteNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const validatedParams = deleteNotificationParamValidation.parse(req.params);
|
||||
|
||||
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);
|
||||
await this.notificationsService.deleteById(validatedParams.id, teamId);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Notification deleted successfully",
|
||||
@@ -113,12 +93,9 @@ class NotificationController {
|
||||
getNotificationById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const notificationId = req.params.id as string;
|
||||
if (!notificationId) {
|
||||
throw new AppError({ message: "Notification ID is required", status: 400 });
|
||||
}
|
||||
const validatedParams = getNotificationByIdParamValidation.parse(req.params);
|
||||
|
||||
const notification = await this.notificationsService.findById(notificationId, teamId);
|
||||
const notification = await this.notificationsService.findById(validatedParams.id, teamId);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -132,14 +109,13 @@ class NotificationController {
|
||||
|
||||
editNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
createNotificationBodyValidation.parse(req.body);
|
||||
const validatedBody = createNotificationBodyValidation.parse(req.body);
|
||||
const validatedParams = editNotificationParamValidation.parse(req.params);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const notificationId = req.params.id as string;
|
||||
if (!notificationId) {
|
||||
throw new AppError({ message: "Notification ID is required", status: 400 });
|
||||
}
|
||||
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body);
|
||||
const notificationId = validatedParams.id;
|
||||
|
||||
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Notification updated successfully",
|
||||
@@ -152,13 +128,11 @@ class NotificationController {
|
||||
|
||||
testAllNotifications = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const monitorId = req.body.monitorId;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const validatedBody = testAllNotificationsBodyValidation.parse(req.body);
|
||||
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId);
|
||||
|
||||
const notifications = monitor.notifications;
|
||||
if (notifications.length === 0) {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
import { logger } from "@/utils/logger.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> {
|
||||
const SERVICE_NAME = "Migration:MigrateStatusPageTypeToArray";
|
||||
|
||||
try {
|
||||
logger.info({ service: SERVICE_NAME, message: "Starting migration of StatusPage type field from string to array" });
|
||||
|
||||
const db = mongoose.connection.db;
|
||||
if (!db) {
|
||||
throw new Error("Database connection is not initialized");
|
||||
}
|
||||
|
||||
// Find all status pages where `type` is a string (not yet migrated)
|
||||
const result = await StatusPageModel.updateMany(
|
||||
{
|
||||
$expr: {
|
||||
$eq: [{ $type: "$type" }, "string"], // only scalar string, not existing array
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
$set: {
|
||||
type: ["$type"],
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
if (result.modifiedCount === 0) {
|
||||
logger.info({ service: SERVICE_NAME, message: "No StatusPage documents needed migration" });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info({
|
||||
service: SERVICE_NAME,
|
||||
message: `Migration complete. Converted ${result.modifiedCount} StatusPage document(s) from string type to array type`,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.error({ service: SERVICE_NAME, message: `Error during StatusPage type migration: ${errorMessage}` });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
|
||||
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
|
||||
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
|
||||
import MigrationModel from "../models/Migration.js";
|
||||
import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js";
|
||||
|
||||
type MigrationEntry = {
|
||||
name: string;
|
||||
@@ -15,7 +14,6 @@ const migrations: MigrationEntry[] = [
|
||||
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
|
||||
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
|
||||
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
|
||||
{ name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray },
|
||||
];
|
||||
|
||||
const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
||||
|
||||
@@ -43,9 +43,9 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: [String],
|
||||
type: String,
|
||||
required: true,
|
||||
default: [],
|
||||
default: "uptime",
|
||||
enum: StatusPageTypes,
|
||||
},
|
||||
companyName: {
|
||||
@@ -100,10 +100,6 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showInfrastructure: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
customCSS: {
|
||||
type: String,
|
||||
default: "",
|
||||
|
||||
@@ -5,12 +5,12 @@ export interface IMaintenanceWindowsRepository {
|
||||
// fetch
|
||||
findById(id: string, teamId: string): Promise<MaintenanceWindow>;
|
||||
findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>;
|
||||
findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise<MaintenanceWindow[]>;
|
||||
findByTeamId(teamId: string, page: number, rowsPerPage: number, field?: string, order?: string, active?: boolean): Promise<MaintenanceWindow[]>;
|
||||
|
||||
// update
|
||||
updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>;
|
||||
// delete
|
||||
deleteById(id: string, teamId: string): Promise<MaintenanceWindow>;
|
||||
// other
|
||||
countByTeamId(teamId: string, active: boolean): Promise<number>;
|
||||
countByTeamId(teamId: string, active?: boolean): Promise<number>;
|
||||
}
|
||||
@@ -75,11 +75,11 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
|
||||
|
||||
findByTeamId = async (
|
||||
teamId: string,
|
||||
active: boolean,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
field: string,
|
||||
order: string
|
||||
field?: string,
|
||||
order?: string,
|
||||
active?: boolean
|
||||
): Promise<MaintenanceWindow[]> => {
|
||||
const maintenanceQuery: Record<string, any> = { teamId };
|
||||
|
||||
@@ -126,7 +126,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
|
||||
}
|
||||
return this.toEntity(deleted);
|
||||
};
|
||||
countByTeamId = async (teamId: string, active: boolean) => {
|
||||
countByTeamId = async (teamId: string, active?: boolean) => {
|
||||
const maintenanceQuery: Record<string, any> = { teamId };
|
||||
|
||||
if (active !== undefined) maintenanceQuery.active = active;
|
||||
|
||||
@@ -57,7 +57,6 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
|
||||
showCharts: doc.showCharts,
|
||||
showUptimePercentage: doc.showUptimePercentage,
|
||||
showAdminLoginLink: doc.showAdminLoginLink,
|
||||
showInfrastructure: doc.showInfrastructure,
|
||||
customCSS: doc.customCSS,
|
||||
createdAt: this.toDateString(doc.createdAt),
|
||||
updatedAt: this.toDateString(doc.updatedAt),
|
||||
|
||||
@@ -50,7 +50,6 @@ class MonitorRoutes {
|
||||
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("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail);
|
||||
this.router.get("/games", this.monitorController.getAllGames);
|
||||
|
||||
// Individual monitor CRUD routes
|
||||
|
||||
@@ -2,7 +2,6 @@ const SERVICE_NAME = "incidentService";
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import type { MonitorStatusResponse } from "@/types/network.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ParseBoolean } from "@/utils/utils.js";
|
||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
||||
import type { Incident, IncidentSummary, User } from "@/types/index.js";
|
||||
@@ -22,13 +21,13 @@ export interface IIncidentService {
|
||||
teamId: string,
|
||||
sortOrder: string,
|
||||
dateRange: string,
|
||||
page: string,
|
||||
rowsPerPage: string,
|
||||
status: string,
|
||||
monitorId: string,
|
||||
resolutionType: string
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
status: boolean | undefined,
|
||||
monitorId: string | undefined,
|
||||
resolutionType: string | undefined
|
||||
): Promise<{ incidents: Incident[]; count: number }>;
|
||||
getIncidentSummary(teamId: string, limit?: string): Promise<IncidentSummary>;
|
||||
getIncidentSummary(teamId: string, limit?: number): Promise<IncidentSummary>;
|
||||
getIncidentById(incidentId: string, teamId: string): Promise<{ incident: Incident; monitor: Monitor; user: User | null }>;
|
||||
}
|
||||
|
||||
@@ -164,13 +163,13 @@ export class IncidentService implements IIncidentService {
|
||||
});
|
||||
|
||||
return resolvedIncident;
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
this.logger.error({
|
||||
service: SERVICE_NAME,
|
||||
method: "resolveIncident",
|
||||
message: error.message,
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
details: { id: incidentId },
|
||||
stack: error.stack,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -180,11 +179,11 @@ export class IncidentService implements IIncidentService {
|
||||
teamId: string,
|
||||
sortOrder: string,
|
||||
dateRange: string,
|
||||
page: string,
|
||||
rowsPerPage: string,
|
||||
status: string,
|
||||
monitorId: string,
|
||||
resolutionType: string
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
status: boolean | undefined,
|
||||
monitorId: string | undefined,
|
||||
resolutionType: string | undefined
|
||||
) => {
|
||||
try {
|
||||
if (!teamId) {
|
||||
@@ -193,9 +192,8 @@ export class IncidentService implements IIncidentService {
|
||||
|
||||
const startDate = getDateForRange(dateRange);
|
||||
|
||||
const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
|
||||
const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20;
|
||||
const parsedStatus = typeof status === "undefined" ? undefined : ParseBoolean(status);
|
||||
const parsedPage = page ?? 0;
|
||||
const parsedRowsPerPage = rowsPerPage ?? 20;
|
||||
|
||||
const incidents = await this.incidentsRepository.findByTeamId(
|
||||
teamId,
|
||||
@@ -203,43 +201,43 @@ export class IncidentService implements IIncidentService {
|
||||
parsedPage,
|
||||
parsedRowsPerPage,
|
||||
sortOrder,
|
||||
parsedStatus,
|
||||
status,
|
||||
monitorId,
|
||||
resolutionType
|
||||
);
|
||||
|
||||
const count = await this.incidentsRepository.countByTeamId(teamId, startDate, parsedStatus, monitorId, resolutionType);
|
||||
const count = await this.incidentsRepository.countByTeamId(teamId, startDate, status, monitorId, resolutionType);
|
||||
|
||||
return { incidents, count };
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
this.logger.error({
|
||||
service: SERVICE_NAME,
|
||||
method: "getIncidentsByTeam",
|
||||
message: error.message,
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
details: { teamId },
|
||||
stack: error.stack,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getIncidentSummary = async (teamId: string, limit?: string) => {
|
||||
getIncidentSummary = async (teamId: string, limit?: number) => {
|
||||
try {
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentSummary", status: 400 });
|
||||
}
|
||||
|
||||
const parsedLimit = limit && Number.isFinite(parseInt(limit, 10)) ? parseInt(limit, 10) : 10;
|
||||
const parsedLimit = limit ?? 10;
|
||||
const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit);
|
||||
|
||||
return summary;
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
this.logger.error({
|
||||
service: SERVICE_NAME,
|
||||
method: "getIncidentSummary",
|
||||
message: error.message,
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
details: { teamId },
|
||||
stack: error.stack,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -254,13 +252,13 @@ export class IncidentService implements IIncidentService {
|
||||
user = await this.usersRepository.findById(incident.resolvedBy);
|
||||
}
|
||||
return { incident, monitor, user };
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
this.logger.error({
|
||||
service: SERVICE_NAME,
|
||||
method: "getIncidentById",
|
||||
message: error.message,
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
details: { incidentId },
|
||||
stack: error.stack,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||
import type { MaintenanceWindow } from "@/types/index.js";
|
||||
import { ParseBoolean } from "@/utils/utils.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
const SERVICE_NAME = "maintenanceWindowService";
|
||||
@@ -10,7 +9,11 @@ export interface IMaintenanceWindowService {
|
||||
getMaintenanceWindowById(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
|
||||
getMaintenanceWindowsByTeamId(params: {
|
||||
teamId: string;
|
||||
query: Record<string, string>;
|
||||
active?: boolean;
|
||||
page?: number;
|
||||
rowsPerPage?: number;
|
||||
field?: string;
|
||||
order?: string;
|
||||
}): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>;
|
||||
getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise<MaintenanceWindow[]>;
|
||||
deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
|
||||
@@ -72,14 +75,25 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
|
||||
return await this.maintenanceWindowsRepository.findById(id, teamId);
|
||||
};
|
||||
|
||||
getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => {
|
||||
let { active, page, rowsPerPage, field, order } = query || {};
|
||||
getMaintenanceWindowsByTeamId = async ({
|
||||
teamId,
|
||||
active,
|
||||
page,
|
||||
rowsPerPage,
|
||||
field,
|
||||
order,
|
||||
}: {
|
||||
teamId: string;
|
||||
active?: boolean;
|
||||
page?: number;
|
||||
rowsPerPage?: number;
|
||||
field?: string;
|
||||
order?: string;
|
||||
}) => {
|
||||
page = page ?? 0;
|
||||
rowsPerPage = rowsPerPage ?? 10;
|
||||
|
||||
active = typeof active === "undefined" ? undefined : ParseBoolean(active);
|
||||
page = parseInt(page) || 0;
|
||||
rowsPerPage = parseInt(rowsPerPage) || 10;
|
||||
|
||||
const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, active, page, rowsPerPage, field, order);
|
||||
const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, page, rowsPerPage, field, order, active);
|
||||
const maintenanceWindowCount = await this.maintenanceWindowsRepository.countByTeamId(teamId, active);
|
||||
return { maintenanceWindows, maintenanceWindowCount };
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface IMonitorService {
|
||||
readonly serviceName: string;
|
||||
|
||||
// create
|
||||
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
|
||||
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>;
|
||||
createMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
|
||||
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
|
||||
|
||||
@@ -64,7 +64,7 @@ export interface IMonitorService {
|
||||
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
|
||||
|
||||
// update
|
||||
editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise<Monitor>;
|
||||
editMonitor(args: { teamId: string; monitorId: string; body: Partial<Monitor> }): Promise<Monitor>;
|
||||
pauseMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
||||
|
||||
// delete
|
||||
@@ -72,7 +72,6 @@ export interface IMonitorService {
|
||||
deleteAllMonitors(args: { teamId: string }): Promise<number>;
|
||||
|
||||
// other
|
||||
sendTestEmail(args: { to: string }): Promise<string>;
|
||||
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
|
||||
importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): Promise<{ imported: number; errors: string[] }>;
|
||||
}
|
||||
@@ -430,7 +429,7 @@ export class MonitorService implements IMonitorService {
|
||||
return groups;
|
||||
};
|
||||
|
||||
editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => {
|
||||
editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Partial<Monitor> }) => {
|
||||
const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body);
|
||||
await this.jobQueue.updateJob(editedMonitor);
|
||||
return editedMonitor;
|
||||
@@ -509,20 +508,6 @@ export class MonitorService implements IMonitorService {
|
||||
return deletedCount;
|
||||
};
|
||||
|
||||
sendTestEmail = async ({ to }: { to: string }): Promise<string> => {
|
||||
const subject = "Test email from Checkmate";
|
||||
const context = { testName: "Monitoring System" };
|
||||
|
||||
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
||||
const messageId = await this.emailService.sendEmail(to, subject, html);
|
||||
|
||||
if (!messageId) {
|
||||
throw new AppError({ message: "Failed to send test email.", service: SERVICE_NAME, method: "sendTestEmail", status: 500 });
|
||||
}
|
||||
|
||||
return messageId;
|
||||
};
|
||||
|
||||
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<any[]> => {
|
||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
|
||||
|
||||
|
||||
@@ -3,5 +3,5 @@ import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
|
||||
export interface INotificationProvider {
|
||||
sendMessage: (notification: Notification, message: NotificationMessage) => Promise<boolean>;
|
||||
sendTestAlert(notification: Notification): Promise<boolean>;
|
||||
sendTestAlert(notification: Partial<Notification>): Promise<boolean>;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export class DiscordProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
sendTestAlert = async (notification: Notification) => {
|
||||
sendTestAlert = async (notification: Partial<Notification>) => {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export class EmailProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
||||
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||
const subject = "Test notification";
|
||||
const html = await buildTestEmail(this.emailService);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export class MatrixProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
sendTestAlert = async (notification: Notification) => {
|
||||
sendTestAlert = async (notification: Partial<Notification>) => {
|
||||
const { homeserverUrl, accessToken, roomId } = notification;
|
||||
if (!homeserverUrl || !accessToken || !roomId) {
|
||||
return false;
|
||||
|
||||
@@ -14,7 +14,7 @@ export class PagerDutyProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
||||
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||
try {
|
||||
await got.post("https://events.pagerduty.com/v2/enqueue", {
|
||||
json: {
|
||||
|
||||
@@ -13,7 +13,7 @@ export class SlackProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
||||
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export class TeamsProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
||||
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export class WebhookProvider implements INotificationProvider {
|
||||
};
|
||||
}
|
||||
|
||||
sendTestAlert = async (notification: Notification) => {
|
||||
sendTestAlert = async (notification: Partial<Notification>) => {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ import { ILogger } from "@/utils/logger.js";
|
||||
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
||||
|
||||
export interface INotificationsService {
|
||||
createNotification: (notificationData: Partial<Notification>) => Promise<Notification>;
|
||||
createNotification: (notificationData: Partial<Notification>, userId: string, teamId: string) => Promise<Notification>;
|
||||
findById: (id: string, teamId: string) => Promise<Notification>;
|
||||
findNotificationsByTeamId: (teamId: string) => Promise<Notification[]>;
|
||||
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
|
||||
deleteById: (id: string, teamId: string) => Promise<Notification>;
|
||||
handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise<boolean>;
|
||||
|
||||
sendTestNotification: (notification: Notification) => Promise<boolean>;
|
||||
sendTestNotification: (notification: Partial<Notification>) => Promise<boolean>;
|
||||
testAllNotifications: (notificationIds: string[]) => Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ export class NotificationsService implements INotificationsService {
|
||||
return await this.sendNotifications(monitor, monitorStatusResponse, decision);
|
||||
};
|
||||
|
||||
sendTestNotification = async (notification: Notification) => {
|
||||
sendTestNotification = async (notification: Partial<Notification>) => {
|
||||
switch (notification.type) {
|
||||
case "email":
|
||||
return await this.emailProvider.sendTestAlert(notification);
|
||||
@@ -174,7 +174,9 @@ export class NotificationsService implements INotificationsService {
|
||||
return true;
|
||||
};
|
||||
|
||||
createNotification = async (notificationData: Partial<Notification>): Promise<Notification> => {
|
||||
createNotification = async (notificationData: Partial<Notification>, userId: string, teamId: string): Promise<Notification> => {
|
||||
notificationData.userId = userId;
|
||||
notificationData.teamId = teamId;
|
||||
return await this.notificationsRepository.create(notificationData);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupporte
|
||||
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
|
||||
export const MonitorMatchMethods = ["equal", "include", "regex"] as const;
|
||||
export type MonitorMatchMethod = (typeof MonitorMatchMethods)[number] | "";
|
||||
|
||||
export interface Monitor {
|
||||
id: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const StatusPageTypes = ["uptime", "infrastructure"] as const;
|
||||
export const StatusPageTypes = ["uptime"] as const;
|
||||
export type StatusPageType = (typeof StatusPageTypes)[number];
|
||||
|
||||
export interface StatusPageLogo {
|
||||
@@ -15,7 +15,7 @@ export interface StatusPage {
|
||||
id: string;
|
||||
userId: string;
|
||||
teamId: string;
|
||||
type: StatusPageType[];
|
||||
type: StatusPageType;
|
||||
companyName: string;
|
||||
url: string;
|
||||
timezone?: string;
|
||||
@@ -28,7 +28,6 @@ export interface StatusPage {
|
||||
showCharts: boolean;
|
||||
showUptimePercentage: boolean;
|
||||
showAdminLoginLink: boolean;
|
||||
showInfrastructure: boolean;
|
||||
customCSS: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
|
||||
//****************************************
|
||||
// Incident Validations
|
||||
//****************************************
|
||||
|
||||
export const getIncidentsByTeamQueryValidation = z.object({
|
||||
sortOrder: z.enum(["asc", "desc"]),
|
||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
|
||||
page: z.coerce.number().int().min(0),
|
||||
rowsPerPage: z.coerce.number().int().min(1),
|
||||
status: booleanCoercion.optional(),
|
||||
monitorId: z.string().optional(),
|
||||
resolutionType: z.enum(["manual", "automatic"]).optional(),
|
||||
});
|
||||
|
||||
export const getIncidentSummaryQueryValidation = z.object({
|
||||
limit: z.coerce.number().int().min(1).optional(),
|
||||
});
|
||||
@@ -20,3 +20,4 @@ export * from "./settingsValidation.js";
|
||||
export * from "./statusPageValidation.js";
|
||||
export * from "./notificationValidation.js";
|
||||
export * from "./userValidation.js";
|
||||
export * from "./incidentValidation.js";
|
||||
@@ -1,6 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
|
||||
const dateToString = z.coerce.date().transform((d) => d.toISOString());
|
||||
|
||||
//****************************************
|
||||
// Maintenance Window Validations
|
||||
//****************************************
|
||||
@@ -11,10 +13,10 @@ export const createMaintenanceWindowBodyValidation = z.object({
|
||||
active: z.boolean().optional(),
|
||||
duration: z.number().min(1, "Duration is required"),
|
||||
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]),
|
||||
start: z.coerce.date(),
|
||||
end: z.coerce.date(),
|
||||
start: dateToString,
|
||||
end: dateToString,
|
||||
repeat: z.number().min(0, "Repeat must be a non-negative number"),
|
||||
expiry: z.coerce.date().optional(),
|
||||
expiry: dateToString.optional(),
|
||||
});
|
||||
|
||||
export const getMaintenanceWindowByIdParamValidation = z.object({
|
||||
@@ -45,10 +47,10 @@ export const editMaintenanceByIdWindowBodyValidation = z.object({
|
||||
active: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
repeat: z.number().optional(),
|
||||
start: z.coerce.date().optional(),
|
||||
end: z.coerce.date().optional(),
|
||||
expiry: z.coerce.date().optional(),
|
||||
monitors: z.array(z.unknown()).optional(),
|
||||
start: dateToString.optional(),
|
||||
end: dateToString.optional(),
|
||||
expiry: dateToString.optional(),
|
||||
monitors: z.array(z.string()).optional(),
|
||||
duration: z.number().optional(),
|
||||
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(),
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
import { GeoContinents } from "@/types/geoCheck.js";
|
||||
import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js";
|
||||
|
||||
export const getMonitorByIdParamValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
@@ -13,7 +14,7 @@ export const getMonitorByIdQueryValidation = z.object({
|
||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
|
||||
numToDisplay: z.coerce.number().optional(),
|
||||
normalize: booleanCoercion.optional(),
|
||||
continent: z.enum(GeoContinents).optional(),
|
||||
continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(),
|
||||
});
|
||||
|
||||
export const getMonitorsByTeamIdParamValidation = z.object({});
|
||||
@@ -25,14 +26,14 @@ export const getMonitorsByTeamIdQueryValidation = z.object({
|
||||
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])),
|
||||
])
|
||||
.optional(),
|
||||
filter: z.union([z.string(), z.literal(""), z.null()]).optional(),
|
||||
filter: z.union([z.string(), z.literal("")]).optional(),
|
||||
});
|
||||
|
||||
export const getMonitorsWithChecksQueryValidation = z.object({
|
||||
limit: z.coerce.number().int().min(1).max(100).optional(),
|
||||
page: z.coerce.number().int().min(0).optional(),
|
||||
rowsPerPage: z.coerce.number().int().min(1).max(100).optional(),
|
||||
filter: z.union([z.string(), z.literal(""), z.null()]).optional(),
|
||||
filter: z.union([z.string(), z.literal("")]).optional(),
|
||||
field: z.string().optional(),
|
||||
order: z.enum(["asc", "desc"]).optional(),
|
||||
type: z
|
||||
@@ -51,8 +52,8 @@ export const getCertificateParamValidation = z.object({
|
||||
export const createMonitorBodyValidation = z.object({
|
||||
_id: z.string().optional(),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
description: z.union([z.string(), z.null(), z.literal("")]).optional(),
|
||||
type: z.string().min(1, "Type is required"),
|
||||
description: z.union([z.string(), z.literal("")]).optional(),
|
||||
type: z.enum(MonitorTypes, "Invalid monitor type"),
|
||||
statusWindowSize: z.number().min(1).max(20).default(5),
|
||||
statusWindowThreshold: z.number().min(1).max(100).default(60),
|
||||
url: z.string().min(1, "URL is required"),
|
||||
@@ -69,7 +70,7 @@ export const createMonitorBodyValidation = z.object({
|
||||
secret: z.string().optional(),
|
||||
jsonPath: z.union([z.string(), z.literal("")]).optional(),
|
||||
expectedValue: z.union([z.string(), z.literal("")]).optional(),
|
||||
matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(),
|
||||
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
|
||||
gameId: z.union([z.string(), z.literal("")]).optional(),
|
||||
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
|
||||
selectedDisks: z.array(z.string()).optional(),
|
||||
@@ -81,11 +82,11 @@ export const createMonitorBodyValidation = z.object({
|
||||
|
||||
export const editMonitorBodyValidation = z.object({
|
||||
name: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
type: z.enum(MonitorTypes).optional(),
|
||||
url: z.string().optional(),
|
||||
statusWindowSize: z.number().min(1).max(20).default(5),
|
||||
statusWindowThreshold: z.number().min(1).max(100).default(60),
|
||||
description: z.union([z.string(), z.null(), z.literal("")]).optional(),
|
||||
description: z.union([z.string(), z.literal("")]).optional(),
|
||||
interval: z.number().optional(),
|
||||
notifications: z.array(z.string()).optional(),
|
||||
secret: z.string().optional(),
|
||||
@@ -93,7 +94,7 @@ export const editMonitorBodyValidation = z.object({
|
||||
useAdvancedMatching: z.boolean().optional(),
|
||||
jsonPath: z.union([z.string(), z.literal("")]).optional(),
|
||||
expectedValue: z.union([z.string(), z.literal("")]).optional(),
|
||||
matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(),
|
||||
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
|
||||
port: z.number().min(1).max(65535).optional(),
|
||||
cpuAlertThreshold: z.number().optional(),
|
||||
memoryAlertThreshold: z.number().optional(),
|
||||
@@ -112,6 +113,62 @@ export const pauseMonitorParamValidation = z.object({
|
||||
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"]).optional(),
|
||||
statusWindow: z.array(z.boolean()).optional(),
|
||||
statusWindowSize: z.number().optional(),
|
||||
statusWindowThreshold: z.number().optional(),
|
||||
type: z.enum(MonitorTypes, "Invalid monitor type"),
|
||||
ignoreTlsErrors: z.boolean().optional(),
|
||||
useAdvancedMatching: z.boolean().optional(),
|
||||
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().optional(),
|
||||
interval: z.number().optional(),
|
||||
uptimePercentage: z.number().optional(),
|
||||
notifications: z.array(z.string()).optional(),
|
||||
secret: z.string().optional(),
|
||||
cpuAlertThreshold: z.number().optional(),
|
||||
cpuAlertCounter: z.number().optional(),
|
||||
memoryAlertThreshold: z.number().optional(),
|
||||
memoryAlertCounter: z.number().optional(),
|
||||
diskAlertThreshold: z.number().optional(),
|
||||
diskAlertCounter: z.number().optional(),
|
||||
tempAlertThreshold: z.number().optional(),
|
||||
tempAlertCounter: z.number().optional(),
|
||||
selectedDisks: z.array(z.string()).optional(),
|
||||
gameId: z.union([z.string(), z.literal("")]).optional(),
|
||||
grpcServiceName: z.union([z.string(), z.literal("")]).optional(),
|
||||
group: z.union([z.string(), z.null()]).optional(),
|
||||
geoCheckEnabled: z.boolean().optional(),
|
||||
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
|
||||
geoCheckInterval: z.number().optional(),
|
||||
recentChecks: z.array(z.unknown()).optional(),
|
||||
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 const getHardwareDetailsByIdParamValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
});
|
||||
|
||||
@@ -67,6 +67,22 @@ 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({
|
||||
to: z.string().min(1, "To field is required"),
|
||||
systemEmailHost: z.string().optional(),
|
||||
|
||||
@@ -10,13 +10,13 @@ export const getStatusPageParamValidation = z.object({
|
||||
});
|
||||
|
||||
export const getStatusPageQueryValidation = z.object({
|
||||
type: z.array(z.enum(["uptime", "infrastructure"])),
|
||||
type: z.literal("uptime"),
|
||||
timeFrame: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
export const createStatusPageBodyValidation = z
|
||||
.object({
|
||||
type: z.array(z.enum(["uptime", "infrastructure"])),
|
||||
type: z.literal("uptime"),
|
||||
companyName: z.string().min(1, "Company name is required"),
|
||||
url: z.string().regex(/^[a-zA-Z0-9_-]+$/, {
|
||||
message: "URL can only contain letters, numbers, underscores, and hyphens",
|
||||
|
||||
Reference in new issue
Block a user