Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2170267e8 | ||
|
|
ff1d473b81 | ||
|
|
bd384bde4a | ||
|
|
e7f052dfa0 | ||
|
|
9903c72ff5 | ||
|
|
d7d237d37b | ||
|
|
5d03f6f0f0 | ||
|
|
38e2a64255 | ||
|
|
1f56338697 | ||
|
|
172467c0a6 | ||
|
|
ff2b6dff2b | ||
|
|
9f600f87af | ||
|
|
7eeff7a396 | ||
|
|
afd6a12e7d | ||
|
|
125d70c4b1 | ||
|
|
5ebe46a81c | ||
|
|
19174ab3e5 | ||
|
|
7c953978ed | ||
|
|
db5441d0e2 | ||
|
|
1d529e3be4 | ||
|
|
23247d4319 | ||
|
|
216d9b2996 | ||
|
|
62f1e62a26 | ||
|
|
55bdae77f9 | ||
|
|
82998a7795 | ||
|
|
9fe8b7572f | ||
|
|
a424092fbd | ||
|
|
ad427dc765 | ||
|
|
116eb80fc9 | ||
|
|
c094a5fd73 | ||
|
|
206e28d1b9 | ||
|
|
2f1a164c26 | ||
|
|
124a8c0b07 | ||
|
|
188445c57a | ||
|
|
35f018623f | ||
|
|
d64526fed2 | ||
|
|
9a8a175f7c | ||
|
|
6ecb27d522 | ||
|
|
24b9b64979 | ||
|
|
a4e4e1313e | ||
|
|
990dc7875a | ||
|
|
69e98a51ec | ||
|
|
3063b9cf22 | ||
|
|
bd32854c7a | ||
|
|
0718e63e2f | ||
|
|
cdb70f9171 | ||
|
|
a1068f1d7e | ||
|
|
75eca743b4 | ||
|
|
321ca59380 | ||
|
|
e3ecfe6ad8 | ||
|
|
f4664eec16 | ||
|
|
56f601e34b | ||
|
|
1244ff6d93 | ||
|
|
c0ea804c98 | ||
|
|
c21cf29db6 | ||
|
|
3d0da70ce6 | ||
|
|
feb061240b | ||
|
|
a924227805 | ||
|
|
2144d3f8c0 | ||
|
|
fcd6786149 | ||
|
|
ea968ebf3c | ||
|
|
8a7b852974 | ||
|
|
c8d3e3b70c | ||
|
|
7a90a6f9f4 |
No files matched your search
Generated
+3
-1
@@ -44,7 +44,7 @@
|
|||||||
"redux-persist": "6.0.0",
|
"redux-persist": "6.0.0",
|
||||||
"swr": "^2.3.6",
|
"swr": "^2.3.6",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"zod": "^4.1.11"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "24.5.2",
|
"@types/node": "24.5.2",
|
||||||
@@ -7359,6 +7359,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.3.6",
|
"version": "4.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|||||||
+2
-2
@@ -13,7 +13,7 @@
|
|||||||
"format-check": "prettier --check ."
|
"format-check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*": "npm run format"
|
"**/*": "prettier --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"redux-persist": "6.0.0",
|
"redux-persist": "6.0.0",
|
||||||
"swr": "^2.3.6",
|
"swr": "^2.3.6",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"zod": "^4.1.11"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "24.5.2",
|
"@types/node": "24.5.2",
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import BackgroundSVG from "@/assets/Images/background.svg";
|
|
||||||
import type { RootState } from "@/Types/state";
|
|
||||||
import { OfflineBanner } from "@/Components/design-elements";
|
import { OfflineBanner } from "@/Components/design-elements";
|
||||||
import { setServerUnreachableCallback, get } from "@/Utils/ApiClient";
|
import { setServerUnreachableCallback, get } from "@/Utils/ApiClient";
|
||||||
|
|
||||||
@@ -13,7 +10,6 @@ interface AppLayoutProps {
|
|||||||
|
|
||||||
const AppLayout = ({ children }: AppLayoutProps) => {
|
const AppLayout = ({ children }: AppLayoutProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const mode = useSelector((state: RootState) => state.ui.mode);
|
|
||||||
const [serverUnreachable, setServerUnreachable] = useState(false);
|
const [serverUnreachable, setServerUnreachable] = useState(false);
|
||||||
const retryIntervalRef = useRef<number | null>(null);
|
const retryIntervalRef = useRef<number | null>(null);
|
||||||
|
|
||||||
@@ -47,10 +43,6 @@ const AppLayout = ({ children }: AppLayoutProps) => {
|
|||||||
sx={{
|
sx={{
|
||||||
minHeight: "100vh",
|
minHeight: "100vh",
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none",
|
|
||||||
backgroundSize: "100% 100%",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<OfflineBanner visible={serverUnreachable} />
|
<OfflineBanner visible={serverUnreachable} />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { MonitorType } from "@/Types/Monitor";
|
|||||||
import { Typography, useTheme } from "@mui/material";
|
import { Typography, useTheme } from "@mui/material";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const types = ["http", "ping", "port", "docker", "game", "grpc"];
|
const types = ["http", "ping", "port", "docker", "game", "grpc", "websocket"];
|
||||||
const typeDisplayNames: Record<string, string> = {
|
const typeDisplayNames: Record<string, string> = {
|
||||||
http: "HTTP",
|
http: "HTTP",
|
||||||
ping: "Ping",
|
ping: "Ping",
|
||||||
@@ -15,6 +15,7 @@ const typeDisplayNames: Record<string, string> = {
|
|||||||
docker: "Docker",
|
docker: "Docker",
|
||||||
game: "Game",
|
game: "Game",
|
||||||
grpc: "gRPC",
|
grpc: "gRPC",
|
||||||
|
websocket: "WebSocket",
|
||||||
};
|
};
|
||||||
const statuses = ["up", "down"];
|
const statuses = ["up", "down"];
|
||||||
const states = ["active", "paused"];
|
const states = ["active", "paused"];
|
||||||
|
|||||||
@@ -103,6 +103,14 @@ export const useMonitorForm = ({
|
|||||||
selectedDisks: data?.selectedDisks || [],
|
selectedDisks: data?.selectedDisks || [],
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case "websocket":
|
||||||
|
defaults = {
|
||||||
|
...base,
|
||||||
|
type: "websocket",
|
||||||
|
url: data?.url || "",
|
||||||
|
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
||||||
|
};
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
defaults = {
|
defaults = {
|
||||||
...base,
|
...base,
|
||||||
|
|||||||
@@ -11,12 +11,17 @@ import { ConfigBox, BasePage } from "@/Components/design-elements";
|
|||||||
import { UserRoles } from "@/Types/User";
|
import { UserRoles } from "@/Types/User";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { UserRole, User } from "@/Types/User";
|
import type { UserRole, User } from "@/Types/User";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
import { useEditUserForm } from "@/Hooks/useEditUserForm";
|
import { useEditUserForm } from "@/Hooks/useEditUserForm";
|
||||||
import { useGet, usePatch } from "@/Hooks/UseApi";
|
import { useGet, usePatch, useDelete } from "@/Hooks/UseApi";
|
||||||
import type { EditUserFormData } from "@/Validation/editUser";
|
import type { EditUserFormData } from "@/Validation/editUser";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { DialogInput } from "@/Components/inputs/Dialog";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import type { RootState } from "@/Types/state";
|
||||||
|
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||||
|
import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin";
|
||||||
|
|
||||||
interface RoleOption {
|
interface RoleOption {
|
||||||
id: UserRole;
|
id: UserRole;
|
||||||
@@ -29,6 +34,12 @@ const EditUserPage = () => {
|
|||||||
const { userId } = useParams<{ userId: string }>();
|
const { userId } = useParams<{ userId: string }>();
|
||||||
const { resolver, defaults } = useEditUserForm();
|
const { resolver, defaults } = useEditUserForm();
|
||||||
const { patch, loading: isSaving } = usePatch();
|
const { patch, loading: isSaving } = usePatch();
|
||||||
|
const { deleteFn, loading: isDeleting } = useDelete();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
const currentUser = useSelector((state: RootState) => state.auth.user);
|
||||||
|
const isSuperAdmin = useIsSuperAdmin();
|
||||||
|
const isAdmin = useIsAdmin();
|
||||||
|
|
||||||
const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`);
|
const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`);
|
||||||
|
|
||||||
@@ -60,6 +71,20 @@ const EditUserPage = () => {
|
|||||||
setValue("role", newRoles, { shouldValidate: true });
|
setValue("role", newRoles, { shouldValidate: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canDeleteUser =
|
||||||
|
isAdmin &&
|
||||||
|
userId !== currentUser?.id &&
|
||||||
|
!user?.role?.includes("demo") &&
|
||||||
|
(isSuperAdmin || user?.role?.every((r) => r !== "admin" && r !== "superadmin"));
|
||||||
|
|
||||||
|
const handleDeleteUser = async () => {
|
||||||
|
const result = await deleteFn(`/auth/users/${userId}`);
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
if (result) {
|
||||||
|
navigate("/account", { state: { tab: "team" } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: EditUserFormData) => {
|
const onSubmit = async (data: EditUserFormData) => {
|
||||||
await patch(`/auth/users/${userId}`, data);
|
await patch(`/auth/users/${userId}`, data);
|
||||||
};
|
};
|
||||||
@@ -165,17 +190,45 @@ const EditUserPage = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Stack
|
||||||
type="submit"
|
gap={LAYOUT.XS}
|
||||||
variant="contained"
|
direction="row"
|
||||||
color="primary"
|
justifyContent={"flex-end"}
|
||||||
loading={isSaving}
|
width="100%"
|
||||||
sx={{ alignSelf: "flex-end", minWidth: 100 }}
|
|
||||||
>
|
>
|
||||||
{t("common.buttons.save")}
|
{canDeleteUser && (
|
||||||
</Button>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={() => setShowDeleteDialog(true)}
|
||||||
|
sx={{ minWidth: 100 }}
|
||||||
|
>
|
||||||
|
{t("common.buttons.removeUser")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
loading={isSaving}
|
||||||
|
sx={{ minWidth: 100 }}
|
||||||
|
>
|
||||||
|
{t("common.buttons.save")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
<DialogInput
|
||||||
|
open={showDeleteDialog}
|
||||||
|
title={t("pages.editUser.dialog.removeUser.title")}
|
||||||
|
content={t("pages.editUser.dialog.removeUser.content", {
|
||||||
|
name: `${user?.firstName} ${user?.lastName}`,
|
||||||
|
})}
|
||||||
|
onCancel={() => setShowDeleteDialog(false)}
|
||||||
|
onConfirm={handleDeleteUser}
|
||||||
|
confirmText={t("common.buttons.removeUser")}
|
||||||
|
loading={isDeleting}
|
||||||
|
/>
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Table } from "@/Components/design-elements";
|
import { Table } from "@/Components/design-elements";
|
||||||
import type { Header } from "@/Components/design-elements/Table";
|
import type { Header } from "@/Components/design-elements/Table";
|
||||||
import { useIsSuperAdmin } from "@/Hooks/useIsAdmin";
|
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||||
import type { User } from "@/Types/User";
|
import type { User } from "@/Types/User";
|
||||||
|
|
||||||
interface TeamTableProps {
|
interface TeamTableProps {
|
||||||
@@ -13,7 +13,7 @@ interface TeamTableProps {
|
|||||||
export const TeamTable = ({ users }: TeamTableProps) => {
|
export const TeamTable = ({ users }: TeamTableProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isSuperAdmin = useIsSuperAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
|
|
||||||
const headers: Header<User>[] = [
|
const headers: Header<User>[] = [
|
||||||
{
|
{
|
||||||
@@ -45,7 +45,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleRowClick = (row: User) => {
|
const handleRowClick = (row: User) => {
|
||||||
if (isSuperAdmin) {
|
if (isAdmin) {
|
||||||
navigate(`/account/team/${row.id}`);
|
navigate(`/account/team/${row.id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
|
|||||||
<Table
|
<Table
|
||||||
headers={headers}
|
headers={headers}
|
||||||
data={users}
|
data={users}
|
||||||
onRowClick={isSuperAdmin ? handleRowClick : undefined}
|
onRowClick={isAdmin ? handleRowClick : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -32,7 +32,12 @@ import {
|
|||||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||||
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
|
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
|
||||||
import { useMonitorForm } from "@/Hooks/useMonitorForm";
|
import { useMonitorForm } from "@/Hooks/useMonitorForm";
|
||||||
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
|
import {
|
||||||
|
type Monitor,
|
||||||
|
type MonitorType,
|
||||||
|
type GamesMap,
|
||||||
|
supportsGeoCheck,
|
||||||
|
} from "@/Types/Monitor";
|
||||||
import type { Notification } from "@/Types/Notification";
|
import type { Notification } from "@/Types/Notification";
|
||||||
import type { MonitorFormData } from "@/Validation/monitor";
|
import type { MonitorFormData } from "@/Validation/monitor";
|
||||||
|
|
||||||
@@ -141,6 +146,17 @@ const getGeneralSettingsConfig = (
|
|||||||
showGrpcServiceName: false,
|
showGrpcServiceName: false,
|
||||||
showIgnoreTls: false,
|
showIgnoreTls: false,
|
||||||
},
|
},
|
||||||
|
websocket: {
|
||||||
|
urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"),
|
||||||
|
urlPlaceholder: t("pages.createMonitor.form.general.option.wsUrl.placeholder"),
|
||||||
|
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
|
||||||
|
showUrl: true,
|
||||||
|
showPort: false,
|
||||||
|
showGameSelect: false,
|
||||||
|
showSecret: false,
|
||||||
|
showGrpcServiceName: false,
|
||||||
|
showIgnoreTls: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return configs[type] || configs.http;
|
return configs[type] || configs.http;
|
||||||
};
|
};
|
||||||
@@ -326,6 +342,13 @@ const CreateMonitorPage = () => {
|
|||||||
"pages.createMonitor.form.type.optionGrpcDescription"
|
"pages.createMonitor.form.type.optionGrpcDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<RadioWithDescription
|
||||||
|
value="websocket"
|
||||||
|
label={t("pages.createMonitor.form.type.optionWebSocket")}
|
||||||
|
description={t(
|
||||||
|
"pages.createMonitor.form.type.optionWebSocketDescription"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -742,7 +765,9 @@ const CreateMonitorPage = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(watchedType === "http" || watchedType === "grpc") && (
|
{(watchedType === "http" ||
|
||||||
|
watchedType === "grpc" ||
|
||||||
|
watchedType === "websocket") && (
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={t("pages.createMonitor.form.ignoreTls.title")}
|
title={t("pages.createMonitor.form.ignoreTls.title")}
|
||||||
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
|
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
|
||||||
@@ -885,7 +910,7 @@ const CreateMonitorPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{watchedType === "http" && (
|
{supportsGeoCheck(watchedType) && (
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={t("pages.createMonitor.form.geoChecks.title")}
|
title={t("pages.createMonitor.form.geoChecks.title")}
|
||||||
subtitle={t("pages.createMonitor.form.geoChecks.description")}
|
subtitle={t("pages.createMonitor.form.geoChecks.description")}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useState, useMemo } from "react";
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useGet } from "@/Hooks/UseApi";
|
import { useGet } from "@/Hooks/UseApi";
|
||||||
import type { MonitorDetailsResponse } from "@/Types/Monitor";
|
import { type MonitorDetailsResponse, supportsGeoCheck } from "@/Types/Monitor";
|
||||||
import type { ChecksResponse } from "@/Types/Check";
|
import type { ChecksResponse } from "@/Types/Check";
|
||||||
import type {
|
import type {
|
||||||
GeoChecksResult,
|
GeoChecksResult,
|
||||||
@@ -122,7 +122,7 @@ const UptimeDetailsPage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const geoChecksUrl = useMemo(() => {
|
const geoChecksUrl = useMemo(() => {
|
||||||
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
|
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@@ -141,7 +141,7 @@ const UptimeDetailsPage = () => {
|
|||||||
|
|
||||||
// Fetch paginated geo checks for the table
|
// Fetch paginated geo checks for the table
|
||||||
const geoChecksTableUrl = useMemo(() => {
|
const geoChecksTableUrl = useMemo(() => {
|
||||||
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
|
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const UptimeMonitorsPage = () => {
|
|||||||
const effectiveTypes =
|
const effectiveTypes =
|
||||||
selectedTypes.length > 0
|
selectedTypes.length > 0
|
||||||
? selectedTypes
|
? selectedTypes
|
||||||
: ["http", "ping", "docker", "port", "game", "grpc"];
|
: ["http", "ping", "docker", "port", "game", "grpc", "websocket"];
|
||||||
|
|
||||||
// Build URL for monitors with checks
|
// Build URL for monitors with checks
|
||||||
const monitorsWithChecksUrl = useMemo(() => {
|
const monitorsWithChecksUrl = useMemo(() => {
|
||||||
|
|||||||
@@ -12,10 +12,20 @@ export const MonitorTypes = [
|
|||||||
"port",
|
"port",
|
||||||
"game",
|
"game",
|
||||||
"grpc",
|
"grpc",
|
||||||
|
"websocket",
|
||||||
"unknown",
|
"unknown",
|
||||||
] as const;
|
] as const;
|
||||||
export type MonitorType = (typeof MonitorTypes)[number];
|
export type MonitorType = (typeof MonitorTypes)[number];
|
||||||
|
|
||||||
|
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
|
||||||
|
|
||||||
|
export const supportsGeoCheck = (type: MonitorType | undefined): boolean => {
|
||||||
|
if (!type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return GeoCheckSupportedTypes.includes(type);
|
||||||
|
};
|
||||||
|
|
||||||
export const MonitorStatuses = [
|
export const MonitorStatuses = [
|
||||||
"up",
|
"up",
|
||||||
"down",
|
"down",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const getMonitorPath = (type: MonitorType): string => {
|
|||||||
ping: "uptime",
|
ping: "uptime",
|
||||||
game: "uptime",
|
game: "uptime",
|
||||||
grpc: "uptime",
|
grpc: "uptime",
|
||||||
|
websocket: "uptime",
|
||||||
unknown: "uptime",
|
unknown: "uptime",
|
||||||
docker: "uptime",
|
docker: "uptime",
|
||||||
hardware: "infrastructure",
|
hardware: "infrastructure",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export const typographyLevels = {
|
|||||||
export const colors = {
|
export const colors = {
|
||||||
gray200: "#EFEFEF",
|
gray200: "#EFEFEF",
|
||||||
gray700: "#313131",
|
gray700: "#313131",
|
||||||
|
gray900: "#151518",
|
||||||
|
gray850: "#1c1c21",
|
||||||
blueBlueWave: "#1570EF",
|
blueBlueWave: "#1570EF",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,4 +33,8 @@ export const darkPalette = {
|
|||||||
secondary: {
|
secondary: {
|
||||||
main: colors.gray700,
|
main: colors.gray700,
|
||||||
},
|
},
|
||||||
|
background: {
|
||||||
|
default: colors.gray900,
|
||||||
|
paper: colors.gray850,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@@ -115,6 +115,13 @@ const hardwareSchema = baseSchema.extend({
|
|||||||
selectedDisks: z.array(z.string()),
|
selectedDisks: z.array(z.string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// WebSocket monitor schema
|
||||||
|
const websocketSchema = baseSchema.extend({
|
||||||
|
type: z.literal("websocket"),
|
||||||
|
url: z.string().min(1, "WebSocket URL is required"),
|
||||||
|
ignoreTlsErrors: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
// Discriminated union of all monitor types
|
// Discriminated union of all monitor types
|
||||||
export const monitorSchema = z.discriminatedUnion("type", [
|
export const monitorSchema = z.discriminatedUnion("type", [
|
||||||
httpSchema,
|
httpSchema,
|
||||||
@@ -125,6 +132,7 @@ export const monitorSchema = z.discriminatedUnion("type", [
|
|||||||
grpcSchema,
|
grpcSchema,
|
||||||
pagespeedSchema,
|
pagespeedSchema,
|
||||||
hardwareSchema,
|
hardwareSchema,
|
||||||
|
websocketSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type MonitorFormData = z.infer<typeof monitorSchema>;
|
export type MonitorFormData = z.infer<typeof monitorSchema>;
|
||||||
@@ -139,4 +147,5 @@ export {
|
|||||||
grpcSchema,
|
grpcSchema,
|
||||||
pagespeedSchema,
|
pagespeedSchema,
|
||||||
hardwareSchema,
|
hardwareSchema,
|
||||||
|
websocketSchema,
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,8 @@
|
|||||||
"sendTestEmail": "Send test email",
|
"sendTestEmail": "Send test email",
|
||||||
"exportToJSON": "Export to JSON",
|
"exportToJSON": "Export to JSON",
|
||||||
"importFromJSON": "Import from JSON",
|
"importFromJSON": "Import from JSON",
|
||||||
"clearFilters": "Clear filters"
|
"clearFilters": "Clear filters",
|
||||||
|
"removeUser": "Remove user"
|
||||||
},
|
},
|
||||||
"charts": {
|
"charts": {
|
||||||
"labels": {
|
"labels": {
|
||||||
@@ -487,6 +488,10 @@
|
|||||||
"grpcServiceName": {
|
"grpcServiceName": {
|
||||||
"label": "Service name",
|
"label": "Service name",
|
||||||
"placeholder": "e.g. my.service.v1 (leave empty for overall health)"
|
"placeholder": "e.g. my.service.v1 (leave empty for overall health)"
|
||||||
|
},
|
||||||
|
"wsUrl": {
|
||||||
|
"label": "WebSocket URL",
|
||||||
|
"placeholder": "wss://example.com/socket"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "General settings",
|
"title": "General settings",
|
||||||
@@ -498,6 +503,7 @@
|
|||||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
|
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
|
||||||
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
|
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
|
||||||
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
|
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
|
||||||
|
"websocket": "Enter the WebSocket URL to monitor (e.g., wss://example.com/socket) and add a display name.",
|
||||||
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
|
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -534,6 +540,8 @@
|
|||||||
"optionGameDescription": "Monitor if a specific game server is online.",
|
"optionGameDescription": "Monitor if a specific game server is online.",
|
||||||
"optionGrpc": "gRPC",
|
"optionGrpc": "gRPC",
|
||||||
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
||||||
|
"optionWebSocket": "WebSocket",
|
||||||
|
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
||||||
"optionHttp": "HTTP(S)",
|
"optionHttp": "HTTP(S)",
|
||||||
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
||||||
"optionPing": "Ping",
|
"optionPing": "Ping",
|
||||||
@@ -619,6 +627,12 @@
|
|||||||
"title": "Roles",
|
"title": "Roles",
|
||||||
"description": "Assign roles to the user. Multiple roles can be selected."
|
"description": "Assign roles to the user. Multiple roles can be selected."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"dialog": {
|
||||||
|
"removeUser": {
|
||||||
|
"title": "Remove user",
|
||||||
|
"content": "Are you sure you want to remove {{name}} from your team? This action cannot be undone."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"incidents": {
|
"incidents": {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
export default defineConfig(({}) => {
|
export default defineConfig(({}) => {
|
||||||
let version = "3.5.0";
|
let version = "3.5.1";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base: "/",
|
base: "/",
|
||||||
|
|||||||
Generated
+15
-76
@@ -43,6 +43,7 @@
|
|||||||
"super-simple-scheduler": "1.4.5",
|
"super-simple-scheduler": "1.4.5",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"winston": "^3.13.0",
|
"winston": "^3.13.0",
|
||||||
|
"ws": "^8.19.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
"@types/ping": "0.4.4",
|
"@types/ping": "0.4.4",
|
||||||
"@types/swagger-ui-express": "4.1.8",
|
"@types/swagger-ui-express": "4.1.8",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||||
"@typescript-eslint/parser": "^8.56.1",
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
"c8": "10.1.3",
|
"c8": "10.1.3",
|
||||||
@@ -871,7 +873,6 @@
|
|||||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.28.6",
|
"@babel/code-frame": "^7.28.6",
|
||||||
"@babel/generator": "^7.28.6",
|
"@babel/generator": "^7.28.6",
|
||||||
@@ -1501,7 +1502,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -1546,7 +1546,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -4338,7 +4337,6 @@
|
|||||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/body-parser": "*",
|
"@types/body-parser": "*",
|
||||||
"@types/express-serve-static-core": "^5.0.0",
|
"@types/express-serve-static-core": "^5.0.0",
|
||||||
@@ -4499,7 +4497,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@@ -4665,6 +4662,16 @@
|
|||||||
"@types/webidl-conversions": "*"
|
"@types/webidl-conversions": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.35",
|
"version": "17.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||||
@@ -4727,7 +4734,6 @@
|
|||||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.56.1",
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
"@typescript-eslint/types": "8.56.1",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
@@ -5341,7 +5347,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -5920,7 +5925,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.19",
|
"baseline-browser-mapping": "^2.8.19",
|
||||||
"caniuse-lite": "^1.0.30001751",
|
"caniuse-lite": "^1.0.30001751",
|
||||||
@@ -6863,7 +6867,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
|
||||||
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
|
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssnano-preset-default": "^7.0.9",
|
"cssnano-preset-default": "^7.0.9",
|
||||||
"lilconfig": "^3.1.3"
|
"lilconfig": "^3.1.3"
|
||||||
@@ -7631,7 +7634,6 @@
|
|||||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -7993,7 +7995,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
@@ -9638,7 +9639,6 @@
|
|||||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "30.2.0",
|
"@jest/core": "30.2.0",
|
||||||
"@jest/types": "30.2.0",
|
"@jest/types": "30.2.0",
|
||||||
@@ -12509,7 +12509,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -14443,63 +14442,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svgo": {
|
|
||||||
"version": "3.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz",
|
|
||||||
"integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "^7.2.0",
|
|
||||||
"css-select": "^5.1.0",
|
|
||||||
"css-tree": "^2.3.1",
|
|
||||||
"css-what": "^6.1.0",
|
|
||||||
"csso": "^5.0.5",
|
|
||||||
"picocolors": "^1.0.0",
|
|
||||||
"sax": "^1.5.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"svgo": "bin/svgo"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/svgo"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/svgo/node_modules/commander": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/svgo/node_modules/css-tree": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"mdn-data": "2.0.30",
|
|
||||||
"source-map-js": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/svgo/node_modules/mdn-data": {
|
|
||||||
"version": "2.0.30",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
|
||||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
|
||||||
"license": "CC0-1.0",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/swagger-ui-dist": {
|
"node_modules/swagger-ui-dist": {
|
||||||
"version": "5.30.0",
|
"version": "5.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz",
|
||||||
@@ -14662,7 +14604,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -14831,7 +14772,6 @@
|
|||||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cspotcode/source-map-support": "^0.8.0",
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
"@tsconfig/node10": "^1.0.7",
|
"@tsconfig/node10": "^1.0.7",
|
||||||
@@ -14994,7 +14934,6 @@
|
|||||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -15628,9 +15567,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.18.3",
|
"version": "8.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|||||||
+3
-1
@@ -15,7 +15,7 @@
|
|||||||
"format-check": "prettier --check ."
|
"format-check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*": "npm run format"
|
"**/*": "prettier --write"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -58,6 +58,7 @@
|
|||||||
"super-simple-scheduler": "1.4.5",
|
"super-simple-scheduler": "1.4.5",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"winston": "^3.13.0",
|
"winston": "^3.13.0",
|
||||||
|
"ws": "^8.19.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
"@types/ping": "0.4.4",
|
"@types/ping": "0.4.4",
|
||||||
"@types/swagger-ui-express": "4.1.8",
|
"@types/swagger-ui-express": "4.1.8",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||||
"@typescript-eslint/parser": "^8.56.1",
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
"c8": "10.1.3",
|
"c8": "10.1.3",
|
||||||
|
|||||||
@@ -1,31 +1,66 @@
|
|||||||
import MongoDB from "../db/MongoDB.js";
|
import MongoDB from "../db/MongoDB.js";
|
||||||
import NetworkService from "../service/infrastructure/networkService.js";
|
import { IDb } from "@/db/IDb.js";
|
||||||
import EmailService from "../service/infrastructure/emailService.js";
|
|
||||||
import BufferService from "../service/infrastructure/bufferService.js";
|
|
||||||
import {
|
import {
|
||||||
|
// Service classes
|
||||||
|
NetworkService,
|
||||||
|
EmailService,
|
||||||
|
BufferService,
|
||||||
|
GlobalPingService,
|
||||||
|
SuperSimpleQueue,
|
||||||
|
SuperSimpleQueueHelper,
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
StatusService,
|
StatusService,
|
||||||
|
NotificationMessageBuilder,
|
||||||
|
MonitorService,
|
||||||
|
StatusPageService,
|
||||||
|
UserService,
|
||||||
|
CheckService,
|
||||||
|
GeoChecksService,
|
||||||
|
DiagnosticService,
|
||||||
|
InviteService,
|
||||||
|
MaintenanceWindowService,
|
||||||
|
IncidentService,
|
||||||
|
// Notification providers
|
||||||
WebhookProvider,
|
WebhookProvider,
|
||||||
SlackProvider,
|
SlackProvider,
|
||||||
EmailProvider,
|
EmailProvider,
|
||||||
DiscordProvider,
|
DiscordProvider,
|
||||||
PagerDutyProvider,
|
PagerDutyProvider,
|
||||||
MatrixProvider,
|
MatrixProvider,
|
||||||
|
// Interfaces
|
||||||
|
INetworkService,
|
||||||
|
IEmailService,
|
||||||
|
IBufferService,
|
||||||
|
ISuperSimpleQueue,
|
||||||
INotificationsService,
|
INotificationsService,
|
||||||
|
IStatusService,
|
||||||
|
IMonitorService,
|
||||||
|
IUserService,
|
||||||
|
ICheckService,
|
||||||
|
IGeoChecksService,
|
||||||
|
IDiagnosticService,
|
||||||
|
IInviteService,
|
||||||
|
IMaintenanceWindowService,
|
||||||
|
IStatusPageService,
|
||||||
|
IIncidentService,
|
||||||
|
INotificationMessageBuilder,
|
||||||
|
ISettingsService,
|
||||||
|
EnvConfig,
|
||||||
} from "@/service/index.js";
|
} from "@/service/index.js";
|
||||||
import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
|
||||||
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
// Network providers
|
||||||
import UserService from "../service/business/userService.js";
|
import { PingProvider } from "@/service/infrastructure/network/PingProvider.js";
|
||||||
import CheckService from "../service/business/checkService.js";
|
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
|
||||||
import GeoChecksService from "../service/business/geoChecksService.js";
|
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
|
||||||
import GlobalPingService from "../service/infrastructure/globalPingService.js";
|
import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js";
|
||||||
import DiagnosticService from "../service/business/diagnosticService.js";
|
import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js";
|
||||||
import InviteService from "../service/business/inviteService.js";
|
import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js";
|
||||||
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
|
import { PortProvider } from "@/service/infrastructure/network/PortProvider.js";
|
||||||
import { MonitorService } from "@/service/index.js";
|
import { GameProvider } from "@/service/infrastructure/network/GameProvider.js";
|
||||||
import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js";
|
import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js";
|
||||||
import IncidentService from "../service/business/incidentService.js";
|
import { WebSocketProvider } from "@/service/infrastructure/network/WebSocketProvider.js";
|
||||||
import { NotificationMessageBuilder, INotificationMessageBuilder } from "../service/infrastructure/notificationMessageBuilder.js";
|
|
||||||
|
// Third-party
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
import ping from "ping";
|
import ping from "ping";
|
||||||
@@ -43,8 +78,9 @@ import { games, GameDig } from "gamedig";
|
|||||||
import jmespath from "jmespath";
|
import jmespath from "jmespath";
|
||||||
import * as grpc from "@grpc/grpc-js";
|
import * as grpc from "@grpc/grpc-js";
|
||||||
import * as protoLoader from "@grpc/proto-loader";
|
import * as protoLoader from "@grpc/proto-loader";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
// repositories
|
// Repositories
|
||||||
import {
|
import {
|
||||||
MongoMonitorsRepository,
|
MongoMonitorsRepository,
|
||||||
MongoChecksRepository,
|
MongoChecksRepository,
|
||||||
@@ -73,33 +109,23 @@ import {
|
|||||||
IMaintenanceWindowsRepository,
|
IMaintenanceWindowsRepository,
|
||||||
} from "@/repositories/index.js";
|
} from "@/repositories/index.js";
|
||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
import { EnvConfig } from "@/service/system/settingsService.js";
|
|
||||||
import { PingProvider } from "@/service/infrastructure/network/PingProvider.js";
|
|
||||||
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
|
|
||||||
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
|
|
||||||
import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js";
|
|
||||||
import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js";
|
|
||||||
import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js";
|
|
||||||
import { PortProvider } from "@/service/infrastructure/network/PortProvider.js";
|
|
||||||
import { GameProvider } from "@/service/infrastructure/network/GameProvider.js";
|
|
||||||
import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js";
|
|
||||||
|
|
||||||
export type InitializedServices = {
|
export type InitializedServices = {
|
||||||
settingsService: any;
|
settingsService: ISettingsService;
|
||||||
db: any;
|
db: IDb;
|
||||||
networkService: any;
|
networkService: INetworkService;
|
||||||
emailService: any;
|
emailService: IEmailService;
|
||||||
bufferService: any;
|
bufferService: IBufferService;
|
||||||
statusService: any;
|
statusService: IStatusService;
|
||||||
jobQueue: any;
|
jobQueue: ISuperSimpleQueue;
|
||||||
userService: any;
|
userService: IUserService;
|
||||||
checkService: any;
|
checkService: ICheckService;
|
||||||
geoChecksService: any;
|
geoChecksService: IGeoChecksService;
|
||||||
diagnosticService: any;
|
diagnosticService: IDiagnosticService;
|
||||||
inviteService: any;
|
inviteService: IInviteService;
|
||||||
maintenanceWindowService: any;
|
maintenanceWindowService: IMaintenanceWindowService;
|
||||||
monitorService: any;
|
monitorService: IMonitorService;
|
||||||
incidentService: any;
|
incidentService: IIncidentService;
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
notificationsService: INotificationsService;
|
notificationsService: INotificationsService;
|
||||||
statusPageService: IStatusPageService;
|
statusPageService: IStatusPageService;
|
||||||
@@ -129,7 +155,7 @@ export const initializeServices = async ({
|
|||||||
}: {
|
}: {
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
envSettings: EnvConfig;
|
envSettings: EnvConfig;
|
||||||
settingsService: any;
|
settingsService: ISettingsService;
|
||||||
settingsRepository: ISettingsRepository;
|
settingsRepository: ISettingsRepository;
|
||||||
}): Promise<InitializedServices> => {
|
}): Promise<InitializedServices> => {
|
||||||
// Create DB
|
// Create DB
|
||||||
@@ -152,8 +178,7 @@ export const initializeServices = async ({
|
|||||||
const teamsRepository = new MongoTeamsRepository();
|
const teamsRepository = new MongoTeamsRepository();
|
||||||
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
||||||
|
|
||||||
// Providers
|
// Network providers
|
||||||
|
|
||||||
const pingProvider = new PingProvider(ping);
|
const pingProvider = new PingProvider(ping);
|
||||||
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath));
|
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath));
|
||||||
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger);
|
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger);
|
||||||
@@ -162,10 +187,9 @@ export const initializeServices = async ({
|
|||||||
const portProvider = new PortProvider(net);
|
const portProvider = new PortProvider(net);
|
||||||
const gameProvider = new GameProvider(logger, GameDig);
|
const gameProvider = new GameProvider(logger, GameDig);
|
||||||
const grpcProvider = new GrpcProvider(grpc, protoLoader);
|
const grpcProvider = new GrpcProvider(grpc, protoLoader);
|
||||||
|
const webSocketProvider = new WebSocketProvider(WebSocket);
|
||||||
|
|
||||||
const networkService = new NetworkService(
|
const networkService = new NetworkService(axios, logger, [
|
||||||
axios,
|
|
||||||
logger,
|
|
||||||
pingProvider,
|
pingProvider,
|
||||||
httpProvider,
|
httpProvider,
|
||||||
pageSpeedProvider,
|
pageSpeedProvider,
|
||||||
@@ -173,8 +197,9 @@ export const initializeServices = async ({
|
|||||||
dockerProvider,
|
dockerProvider,
|
||||||
portProvider,
|
portProvider,
|
||||||
gameProvider,
|
gameProvider,
|
||||||
grpcProvider
|
grpcProvider,
|
||||||
);
|
webSocketProvider,
|
||||||
|
]);
|
||||||
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
||||||
|
|
||||||
const notificationMessageBuilder = new NotificationMessageBuilder();
|
const notificationMessageBuilder = new NotificationMessageBuilder();
|
||||||
@@ -196,6 +221,7 @@ export const initializeServices = async ({
|
|||||||
|
|
||||||
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
|
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
|
||||||
|
|
||||||
|
// Notification providers
|
||||||
const webhookProvider = new WebhookProvider(logger);
|
const webhookProvider = new WebhookProvider(logger);
|
||||||
const slackProvider = new SlackProvider(logger);
|
const slackProvider = new SlackProvider(logger);
|
||||||
const emailProvider = new EmailProvider(emailService, logger);
|
const emailProvider = new EmailProvider(emailService, logger);
|
||||||
@@ -279,7 +305,6 @@ export const initializeServices = async ({
|
|||||||
const statusPageService = new StatusPageService(statusPagesRepository);
|
const statusPageService = new StatusPageService(statusPagesRepository);
|
||||||
|
|
||||||
const services = {
|
const services = {
|
||||||
//v1
|
|
||||||
settingsService,
|
settingsService,
|
||||||
db,
|
db,
|
||||||
networkService,
|
networkService,
|
||||||
|
|||||||
@@ -180,6 +180,20 @@ class AuthController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
deleteUserById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
getUserByIdParamValidation.parse(req.params);
|
||||||
|
const targetUserId = req.params.userId;
|
||||||
|
await this.userService.deleteUserById(req.user, targetUserId);
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
msg: "User removed successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
getAllUsers = async (req: Request, res: Response, next: NextFunction) => {
|
getAllUsers = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const allUsers = await this.userService.getAllUsers();
|
const allUsers = await this.userService.getAllUsers();
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IDb {
|
||||||
|
connect(): Promise<void>;
|
||||||
|
disconnect(): Promise<void>;
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
import AppSettings from "./models/AppSettings.js";
|
import AppSettings from "@/db/models/AppSettings.js";
|
||||||
import { runMigrations } from "./migration/index.js";
|
import { runMigrations } from "@/db/migration/index.js";
|
||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
import { EnvConfig } from "@/service/system/settingsService.js";
|
import { EnvConfig } from "@/service/system/settingsService.js";
|
||||||
const SERVICE_NAME = "MongoDB";
|
const SERVICE_NAME = "MongoDB";
|
||||||
class MongoDB {
|
import { IDb } from "@/db/IDb.js";
|
||||||
|
|
||||||
|
class MongoDB implements IDb {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -46,12 +48,12 @@ class MongoDB {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "connect",
|
method: "connect",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -63,12 +65,12 @@ class MongoDB {
|
|||||||
await mongoose.disconnect();
|
await mongoose.disconnect();
|
||||||
this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" });
|
this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" });
|
||||||
return;
|
return;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "disconnect",
|
method: "disconnect",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+3
-4
@@ -9,7 +9,7 @@ import fs from "fs";
|
|||||||
import { runMigrations } from "./db/migration/index.js";
|
import { runMigrations } from "./db/migration/index.js";
|
||||||
|
|
||||||
import Logger, { ILogger } from "@/utils/logger.js";
|
import Logger, { ILogger } from "@/utils/logger.js";
|
||||||
import SettingsService from "@/service/system/settingsService.js";
|
import { SettingsService } from "@/service/index.js";
|
||||||
import { MongoSettingsRepository } from "./repositories/index.js";
|
import { MongoSettingsRepository } from "./repositories/index.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "Server";
|
const SERVICE_NAME = "Server";
|
||||||
@@ -50,9 +50,8 @@ const startApp = async () => {
|
|||||||
openApiSpec,
|
openApiSpec,
|
||||||
});
|
});
|
||||||
|
|
||||||
const port = 52345;
|
const server = app.listen(env.PORT, () => {
|
||||||
const server = app.listen(port, () => {
|
logger.info({ message: `Server started on port:${env.PORT}` });
|
||||||
logger.info({ message: `Server started on port:${port}` });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
initShutdownListener(server, services);
|
initShutdownListener(server, services);
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
|
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
|
||||||
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
|
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
|
||||||
import type { GeoChecksQueryResult, FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
|
import type { FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
|
||||||
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
|
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
|
||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "GeoChecksRepository";
|
const SERVICE_NAME = "GeoChecksRepository";
|
||||||
|
|
||||||
class MongoGeoChecksRepository implements IGeoChecksRepository {
|
class MongoGeoChecksRepository implements IGeoChecksRepository {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
constructor(logger: any) {
|
constructor(logger: ILogger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ class AuthRoutes {
|
|||||||
|
|
||||||
this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers);
|
this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers);
|
||||||
this.router.post("/users", verifyJWT, isAllowed(["superadmin"]), upload.single("profileImage"), this.authController.createUser);
|
this.router.post("/users", verifyJWT, isAllowed(["superadmin"]), upload.single("profileImage"), this.authController.createUser);
|
||||||
this.router.get("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.getUserById);
|
this.router.get("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getUserById);
|
||||||
this.router.patch("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById);
|
this.router.patch("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById);
|
||||||
this.router.patch("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById);
|
this.router.patch("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById);
|
||||||
|
this.router.delete("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.deleteUserById);
|
||||||
|
|
||||||
this.router.patch("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser);
|
this.router.patch("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser);
|
||||||
this.router.delete("/user", verifyJWT, this.authController.deleteUser);
|
this.router.delete("/user", verifyJWT, this.authController.deleteUser);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export interface ICheckService {
|
|||||||
updateChecksTTL(params: { teamId: string; ttl: string }): Promise<void>;
|
updateChecksTTL(params: { teamId: string; ttl: string }): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CheckService implements ICheckService {
|
export class CheckService implements ICheckService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
@@ -194,5 +194,3 @@ class CheckService implements ICheckService {
|
|||||||
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } });
|
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CheckService;
|
|
||||||
@@ -3,15 +3,19 @@ import os from "os";
|
|||||||
|
|
||||||
const SERVICE_NAME = "diagnosticService";
|
const SERVICE_NAME = "diagnosticService";
|
||||||
|
|
||||||
interface MemoryUsageMetrics {
|
export interface IDiagnosticService {
|
||||||
rss: number;
|
getCPUUsage(): Promise<{ userUsageMs: number; systemUsageMs: number; usagePercentage: number }>;
|
||||||
heapTotal: number;
|
getSystemStats(): Promise<{
|
||||||
heapUsed: number;
|
osStats: { freeMemoryBytes: number; totalMemoryBytes: number };
|
||||||
external: number;
|
memoryUsage: Record<keyof NodeJS.MemoryUsage, number>;
|
||||||
arrayBuffers: number;
|
cpuUsage: { userUsageMs: number; systemUsageMs: number; usagePercentage: number };
|
||||||
|
v8HeapStats: { totalHeapSizeBytes: number; usedHeapSizeBytes: number; heapSizeLimitBytes: number };
|
||||||
|
eventLoopDelayMs: number;
|
||||||
|
uptimeMs: number;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DiagnosticService {
|
export class DiagnosticService implements IDiagnosticService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -103,5 +107,3 @@ class DiagnosticService {
|
|||||||
return diagnostics;
|
return diagnostics;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DiagnosticService;
|
|
||||||
@@ -16,7 +16,7 @@ export interface IGeoChecksService {
|
|||||||
getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise<any>;
|
getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeoChecksService implements IGeoChecksService {
|
export class GeoChecksService implements IGeoChecksService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -68,7 +68,7 @@ class GeoChecksService implements IGeoChecksService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Create measurement request
|
// Step 1: Create measurement request
|
||||||
const measurementId = await this.globalPingService.createMeasurement(monitor.url, monitor.geoCheckLocations);
|
const measurementId = await this.globalPingService.createMeasurement(monitor.type, monitor.url, monitor.geoCheckLocations);
|
||||||
|
|
||||||
if (!measurementId) {
|
if (!measurementId) {
|
||||||
// GlobalPing API is down, skip this check
|
// GlobalPing API is down, skip this check
|
||||||
@@ -76,7 +76,6 @@ class GeoChecksService implements IGeoChecksService {
|
|||||||
message: "Skipping geo check due to API unavailability",
|
message: "Skipping geo check due to API unavailability",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "buildGeoCheck",
|
method: "buildGeoCheck",
|
||||||
details: { monitorId: monitor.id },
|
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -90,7 +89,6 @@ class GeoChecksService implements IGeoChecksService {
|
|||||||
message: "No successful geo check results",
|
message: "No successful geo check results",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "buildGeoCheck",
|
method: "buildGeoCheck",
|
||||||
details: { monitorId: monitor.id, measurementId },
|
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -102,17 +100,16 @@ class GeoChecksService implements IGeoChecksService {
|
|||||||
message: `Geo check completed for monitor ${monitor.id}`,
|
message: `Geo check completed for monitor ${monitor.id}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "buildGeoCheck",
|
method: "buildGeoCheck",
|
||||||
details: { monitorId: monitor.id, resultsCount: results.length },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return geoCheck;
|
return geoCheck;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: "Error executing geo check",
|
message: "Error executing geo check",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "buildGeoCheck",
|
method: "buildGeoCheck",
|
||||||
details: { monitorId: monitor.id, error: error.message },
|
details: { monitorId: monitor.id, error: error instanceof Error ? error.message : "Unknown error" },
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -172,21 +169,9 @@ class GeoChecksService implements IGeoChecksService {
|
|||||||
|
|
||||||
const { sortOrder, dateRange, page, rowsPerPage, continent } = query;
|
const { sortOrder, dateRange, page, rowsPerPage, continent } = query;
|
||||||
const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined;
|
const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined;
|
||||||
|
|
||||||
this.logger.debug({
|
|
||||||
message: "getGeoChecksByMonitor query params",
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
method: "getGeoChecksByMonitor",
|
|
||||||
details: { continent, continents, query },
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsedPage = page ? parseInt(page) : page;
|
const parsedPage = page ? parseInt(page) : page;
|
||||||
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
|
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
|
||||||
|
|
||||||
const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents);
|
const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GeoChecksService;
|
|
||||||
@@ -5,12 +5,34 @@ import { AppError } from "@/utils/AppError.js";
|
|||||||
import { ParseBoolean } from "@/utils/utils.js";
|
import { ParseBoolean } from "@/utils/utils.js";
|
||||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||||
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
||||||
import type { Incident } from "@/types/index.js";
|
import type { Incident, IncidentSummary, User } from "@/types/index.js";
|
||||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||||
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
||||||
import type { ILogger } from "@/utils/logger.js";
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
class IncidentService {
|
export interface IIncidentService {
|
||||||
|
handleIncident(
|
||||||
|
monitor: Monitor,
|
||||||
|
code: number,
|
||||||
|
decision: MonitorActionDecision,
|
||||||
|
monitorStatusResponse?: MonitorStatusResponse
|
||||||
|
): Promise<Incident | null>;
|
||||||
|
resolveIncident(incidentId: string, userId: string, teamId: string, comment?: string, userEmail?: string): Promise<Incident>;
|
||||||
|
getIncidentsByTeam(
|
||||||
|
teamId: string,
|
||||||
|
sortOrder: string,
|
||||||
|
dateRange: string,
|
||||||
|
page: string,
|
||||||
|
rowsPerPage: string,
|
||||||
|
status: string,
|
||||||
|
monitorId: string,
|
||||||
|
resolutionType: string
|
||||||
|
): Promise<{ incidents: Incident[]; count: number }>;
|
||||||
|
getIncidentSummary(teamId: string, limit?: string): Promise<IncidentSummary>;
|
||||||
|
getIncidentById(incidentId: string, teamId: string): Promise<{ incident: Incident; monitor: Monitor; user: User | null }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IncidentService implements IIncidentService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -244,5 +266,3 @@ class IncidentService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IncidentService;
|
|
||||||
@@ -5,7 +5,13 @@ import { AppError } from "@/utils/AppError.js";
|
|||||||
|
|
||||||
const SERVICE_NAME = "inviteService";
|
const SERVICE_NAME = "inviteService";
|
||||||
|
|
||||||
class InviteService {
|
export interface IInviteService {
|
||||||
|
getInviteToken(params: { invite: Partial<Invite>; teamId: string; userRoles: UserRole[] }): Promise<Invite>;
|
||||||
|
sendInviteEmail(params: { invite: Partial<Invite>; firstName: string; userRoles: UserRole[] }): Promise<void>;
|
||||||
|
verifyInviteToken(params: { inviteToken: string }): Promise<Invite>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InviteService implements IInviteService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private settingsService: any;
|
private settingsService: any;
|
||||||
@@ -88,5 +94,3 @@ class InviteService {
|
|||||||
return await this.invitesRepository.findByToken(inviteToken);
|
return await this.invitesRepository.findByToken(inviteToken);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InviteService;
|
|
||||||
@@ -1,10 +1,23 @@
|
|||||||
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
|
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||||
|
import type { MaintenanceWindow } from "@/types/index.js";
|
||||||
import { ParseBoolean } from "@/utils/utils.js";
|
import { ParseBoolean } from "@/utils/utils.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "maintenanceWindowService";
|
const SERVICE_NAME = "maintenanceWindowService";
|
||||||
|
|
||||||
class MaintenanceWindowService {
|
export interface IMaintenanceWindowService {
|
||||||
|
createMaintenanceWindow(params: { teamId: string; body: Partial<MaintenanceWindow> & { monitors: string[] } }): Promise<void>;
|
||||||
|
getMaintenanceWindowById(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
|
||||||
|
getMaintenanceWindowsByTeamId(params: {
|
||||||
|
teamId: string;
|
||||||
|
query: Record<string, string>;
|
||||||
|
}): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>;
|
||||||
|
getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise<MaintenanceWindow[]>;
|
||||||
|
deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
|
||||||
|
editMaintenanceWindow(params: { id: string; teamId: string; body: Partial<MaintenanceWindow> }): Promise<MaintenanceWindow>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MaintenanceWindowService implements IMaintenanceWindowService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||||
@@ -83,5 +96,3 @@ class MaintenanceWindowService {
|
|||||||
return await this.maintenanceWindowsRepository.updateById(id, teamId, body);
|
return await this.maintenanceWindowsRepository.updateById(id, teamId, body);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MaintenanceWindowService;
|
|
||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
PageSpeedDetailsResult,
|
PageSpeedDetailsResult,
|
||||||
GamesMap,
|
GamesMap,
|
||||||
} from "@/types/monitor.js";
|
} from "@/types/monitor.js";
|
||||||
|
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||||
import type { GeoContinent } from "@/types/geoCheck.js";
|
import type { GeoContinent } from "@/types/geoCheck.js";
|
||||||
import type {
|
import type {
|
||||||
IChecksRepository,
|
IChecksRepository,
|
||||||
@@ -226,7 +227,8 @@ export class MonitorService implements IMonitorService {
|
|||||||
checksData.monitorType !== "docker" &&
|
checksData.monitorType !== "docker" &&
|
||||||
checksData.monitorType !== "port" &&
|
checksData.monitorType !== "port" &&
|
||||||
checksData.monitorType !== "game" &&
|
checksData.monitorType !== "game" &&
|
||||||
checksData.monitorType !== "grpc"
|
checksData.monitorType !== "grpc" &&
|
||||||
|
checksData.monitorType !== "websocket"
|
||||||
) {
|
) {
|
||||||
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
||||||
}
|
}
|
||||||
@@ -340,7 +342,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (monitor.type !== "http" || !monitor.geoCheckEnabled) {
|
if (!supportsGeoCheck(monitor.type) || !monitor.geoCheckEnabled) {
|
||||||
return { groupedGeoChecks: [] };
|
return { groupedGeoChecks: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,32 @@ import { canManageRole, type UserRole } from "@/types/user.js";
|
|||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||||
|
import { IEmailService } from "@/service/infrastructure/emailService.js";
|
||||||
|
import { ISettingsService } from "@/service/system/settingsService.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
type JwtType = typeof jwt;
|
||||||
const SERVICE_NAME = "userService";
|
const SERVICE_NAME = "userService";
|
||||||
|
|
||||||
class UserService {
|
export interface IUserService {
|
||||||
|
issueToken(payload: Partial<User>, appSettings: { jwtTTL?: string; jwtSecret?: string }): string;
|
||||||
|
registerUser(user: Partial<User>, inviteToken: string, file: unknown): Promise<{ user: User; token: string }>;
|
||||||
|
createUser(userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: unknown): Promise<User>;
|
||||||
|
loginUser(email: string, password: string): Promise<{ user: User; token: string }>;
|
||||||
|
editUser(updates: Partial<User & { newPassword?: string }>, file: unknown, currentUser: User): Promise<User>;
|
||||||
|
checkSuperadminExists(): Promise<boolean>;
|
||||||
|
requestRecovery(email: string): Promise<string | false | undefined>;
|
||||||
|
validateRecovery(recoveryToken: string): Promise<void>;
|
||||||
|
resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>;
|
||||||
|
deleteUser(user: User): Promise<void>;
|
||||||
|
deleteUserById(actor: User, targetUserId: string): Promise<void>;
|
||||||
|
getAllUsers(): Promise<User[]>;
|
||||||
|
getUserById(roles: UserRole[], userId: string): Promise<User>;
|
||||||
|
editUserById(userId: string, patch: Partial<User>): Promise<void>;
|
||||||
|
setPasswordByUserId(userId: string, password: string): Promise<User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserService implements IUserService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private hashPassword = (password: string): string => {
|
private hashPassword = (password: string): string => {
|
||||||
@@ -22,10 +44,10 @@ class UserService {
|
|||||||
return bcrypt.hashSync(password, salt);
|
return bcrypt.hashSync(password, salt);
|
||||||
};
|
};
|
||||||
|
|
||||||
private emailService: any;
|
private emailService: IEmailService;
|
||||||
private settingsService: any;
|
private settingsService: ISettingsService;
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
private jwt: any;
|
private jwt: JwtType;
|
||||||
private jobQueue: ISuperSimpleQueue;
|
private jobQueue: ISuperSimpleQueue;
|
||||||
private crypto: any;
|
private crypto: any;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
@@ -50,10 +72,10 @@ class UserService {
|
|||||||
teamsRepository,
|
teamsRepository,
|
||||||
}: {
|
}: {
|
||||||
crypto: any;
|
crypto: any;
|
||||||
emailService: any;
|
emailService: IEmailService;
|
||||||
settingsService: any;
|
settingsService: ISettingsService;
|
||||||
logger: any;
|
logger: ILogger;
|
||||||
jwt: any;
|
jwt: JwtType;
|
||||||
jobQueue: ISuperSimpleQueue;
|
jobQueue: ISuperSimpleQueue;
|
||||||
monitorsRepository: IMonitorsRepository;
|
monitorsRepository: IMonitorsRepository;
|
||||||
usersRepository: IUsersRepository;
|
usersRepository: IUsersRepository;
|
||||||
@@ -120,7 +142,7 @@ class UserService {
|
|||||||
message: "New user created",
|
message: "New user created",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "registerUser",
|
method: "registerUser",
|
||||||
details: newUser.id,
|
details: { userId: newUser.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
delete newUser.profileImage;
|
delete newUser.profileImage;
|
||||||
@@ -134,20 +156,23 @@ class UserService {
|
|||||||
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
|
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
|
||||||
name: newUser.firstName,
|
name: newUser.firstName,
|
||||||
});
|
});
|
||||||
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: any) => {
|
if (!html) {
|
||||||
|
throw new Error("Failed to build welcome email HTML");
|
||||||
|
}
|
||||||
|
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: unknown) => {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "registerUser",
|
method: "registerUser",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "registerUser",
|
method: "registerUser",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +206,7 @@ class UserService {
|
|||||||
message: "New user created by superadmin",
|
message: "New user created by superadmin",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "createUser",
|
method: "createUser",
|
||||||
details: newUser.id,
|
details: { userId: newUser.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
newUser.profileImage = undefined;
|
newUser.profileImage = undefined;
|
||||||
@@ -256,6 +281,14 @@ class UserService {
|
|||||||
email,
|
email,
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
|
if (!html) {
|
||||||
|
throw new AppError({
|
||||||
|
message: "Failed to build password reset email HTML",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "requestRecovery",
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html);
|
const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html);
|
||||||
return msgId;
|
return msgId;
|
||||||
};
|
};
|
||||||
@@ -313,25 +346,65 @@ class UserService {
|
|||||||
|
|
||||||
if (roles.includes("superadmin")) {
|
if (roles.includes("superadmin")) {
|
||||||
// 2. Remove all jobs, delete checks and alerts
|
// 2. Remove all jobs, delete checks and alerts
|
||||||
res &&
|
if (res && res.length > 0) {
|
||||||
res?.length > 0 &&
|
await Promise.all(
|
||||||
(await Promise.all(
|
|
||||||
res.map(async (monitor) => {
|
res.map(async (monitor) => {
|
||||||
await this.jobQueue.deleteJob(monitor.id);
|
await this.jobQueue.deleteJob(monitor.id);
|
||||||
})
|
})
|
||||||
));
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 6. Delete the user by id
|
// 6. Delete the user by id
|
||||||
await this.usersRepository.deleteById(userId);
|
await this.usersRepository.deleteById(userId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
deleteUserById = async (actor: User, targetUserId: string) => {
|
||||||
|
if (actor.id === targetUserId) {
|
||||||
|
throw new AppError({ message: "Cannot delete your own account from here", service: SERVICE_NAME, method: "deleteUserById", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUser = await this.usersRepository.findById(targetUserId);
|
||||||
|
|
||||||
|
if (targetUser.teamId !== actor.teamId) {
|
||||||
|
throw new AppError({ message: "User is not on your team", service: SERVICE_NAME, method: "deleteUserById", status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUser.role.includes("demo")) {
|
||||||
|
throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUserById", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const actorRoles = actor.role;
|
||||||
|
const targetRoles = targetUser.role;
|
||||||
|
|
||||||
|
// Check actor can manage all of target's roles
|
||||||
|
for (const targetRole of targetRoles) {
|
||||||
|
const canManage = actorRoles.some((actorRole) => canManageRole(actorRole, targetRole));
|
||||||
|
if (!canManage) {
|
||||||
|
throw new AppError({
|
||||||
|
message: "You do not have permission to remove this user",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "deleteUserById",
|
||||||
|
status: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.usersRepository.deleteById(targetUserId);
|
||||||
|
|
||||||
|
this.logger.info({
|
||||||
|
message: `User ${targetUserId} deleted by ${actor.id}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "deleteUserById",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
getAllUsers = async () => {
|
getAllUsers = async () => {
|
||||||
return await this.usersRepository.findAll();
|
return await this.usersRepository.findAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
getUserById = async (roles: any, userId: any) => {
|
getUserById = async (roles: any, userId: any) => {
|
||||||
if (!roles.includes("superadmin")) {
|
if (!roles.includes("superadmin") && !roles.includes("admin")) {
|
||||||
throw new AppError({ message: "User is not a superadmin", service: SERVICE_NAME, status: 403 });
|
throw new AppError({ message: "Insufficient permissions", service: SERVICE_NAME, status: 403 });
|
||||||
}
|
}
|
||||||
const user = await this.usersRepository.findById(userId);
|
const user = await this.usersRepository.findById(userId);
|
||||||
|
|
||||||
@@ -348,4 +421,3 @@ class UserService {
|
|||||||
return updatedUser;
|
return updatedUser;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export default UserService;
|
|
||||||
@@ -1,14 +1,33 @@
|
|||||||
|
// Business services
|
||||||
|
export * from "@/service/business/checkService.js";
|
||||||
|
export * from "@/service/business/diagnosticService.js";
|
||||||
|
export * from "@/service/business/geoChecksService.js";
|
||||||
|
export * from "@/service/business/incidentService.js";
|
||||||
|
export * from "@/service/business/inviteService.js";
|
||||||
|
export * from "@/service/business/maintenanceWindowService.js";
|
||||||
export * from "@/service/business/monitorService.js";
|
export * from "@/service/business/monitorService.js";
|
||||||
|
export * from "@/service/business/statusPageService.js";
|
||||||
|
export * from "@/service/business/userService.js";
|
||||||
|
|
||||||
|
// Infrastructure services
|
||||||
|
export * from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||||
|
export * from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||||
|
export * from "@/service/infrastructure/notificationMessageBuilder.js";
|
||||||
|
export * from "@/service/infrastructure/bufferService.js";
|
||||||
|
export * from "@/service/infrastructure/emailService.js";
|
||||||
|
export * from "@/service/infrastructure/globalPingService.js";
|
||||||
export * from "@/service/infrastructure/networkService.js";
|
export * from "@/service/infrastructure/networkService.js";
|
||||||
|
export * from "@/service/infrastructure/notificationsService.js";
|
||||||
|
export * from "@/service/infrastructure/statusService.js";
|
||||||
|
|
||||||
|
// Notification providers
|
||||||
export * from "@/service/infrastructure/notificationProviders/discord.js";
|
export * from "@/service/infrastructure/notificationProviders/discord.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/email.js";
|
export * from "@/service/infrastructure/notificationProviders/email.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js";
|
export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/pagerduty.js";
|
|
||||||
export * from "@/service/infrastructure/notificationProviders/matrix.js";
|
export * from "@/service/infrastructure/notificationProviders/matrix.js";
|
||||||
|
export * from "@/service/infrastructure/notificationProviders/pagerduty.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/slack.js";
|
export * from "@/service/infrastructure/notificationProviders/slack.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
||||||
export * from "@/service/infrastructure/notificationsService.js";
|
|
||||||
export * from "@/service/infrastructure/statusService.js";
|
// System services
|
||||||
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
export * from "@/service/system/settingsService.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/matrix.js";
|
|
||||||
export * from "@/service/business/statusPageService.js";
|
|
||||||
@@ -2,6 +2,7 @@ import { IMonitorsRepository } from "@/repositories/index.js";
|
|||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
import Scheduler from "super-simple-scheduler";
|
import Scheduler from "super-simple-scheduler";
|
||||||
import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||||
|
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||||
const SERVICE_NAME = "JobQueue";
|
const SERVICE_NAME = "JobQueue";
|
||||||
|
|
||||||
type QueueJobFailure = {
|
type QueueJobFailure = {
|
||||||
@@ -53,7 +54,7 @@ export interface ISuperSimpleQueue {
|
|||||||
obliterate(): Promise<void>;
|
obliterate(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SuperSimpleQueue implements ISuperSimpleQueue {
|
export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -125,8 +126,13 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
data: monitor,
|
data: monitor,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Return early if we don't need geo checks
|
||||||
|
if (!supportsGeoCheck(monitor.type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add geo check job if enabled for HTTP monitors
|
// Add geo check job if enabled for HTTP monitors
|
||||||
if (monitor.geoCheckEnabled && monitor.type === "http") {
|
if (monitor.geoCheckEnabled) {
|
||||||
this.scheduler.addJob({
|
this.scheduler.addJob({
|
||||||
id: `${monitorId}-geo`,
|
id: `${monitorId}-geo`,
|
||||||
template: "geo-check-job",
|
template: "geo-check-job",
|
||||||
@@ -175,7 +181,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
|
|
||||||
// Handle geo check job lifecycle
|
// Handle geo check job lifecycle
|
||||||
const geoJobId = `${monitor.id}-geo`;
|
const geoJobId = `${monitor.id}-geo`;
|
||||||
if (monitor.geoCheckEnabled && monitor.type === "http") {
|
if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) {
|
||||||
// Check if geo job exists
|
// Check if geo job exists
|
||||||
const existingGeoJob = await this.scheduler.getJob(geoJobId);
|
const existingGeoJob = await this.scheduler.getJob(geoJobId);
|
||||||
if (existingGeoJob) {
|
if (existingGeoJob) {
|
||||||
@@ -289,5 +295,3 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SuperSimpleQueue;
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
const SERVICE_NAME = "JobQueueHelper";
|
const SERVICE_NAME = "JobQueueHelper";
|
||||||
import type { Monitor } from "@/types/monitor.js";
|
import type { Monitor } from "@/types/monitor.js";
|
||||||
|
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
|
import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js";
|
||||||
import type { StatusChangeResult } from "@/types/index.js";
|
import type { StatusChangeResult } from "@/types/index.js";
|
||||||
import IncidentService from "@/service/business/incidentService.js";
|
|
||||||
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
|
||||||
import {
|
import {
|
||||||
IMaintenanceWindowsRepository,
|
IMaintenanceWindowsRepository,
|
||||||
IMonitorsRepository,
|
IMonitorsRepository,
|
||||||
@@ -15,7 +14,7 @@ import {
|
|||||||
IGeoChecksRepository,
|
IGeoChecksRepository,
|
||||||
} from "@/repositories/index.js";
|
} from "@/repositories/index.js";
|
||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
import { IBufferService } from "../bufferService.js";
|
import { IBufferService } from "@/service/index.js";
|
||||||
|
|
||||||
export interface ISuperSimpleQueueHelper {
|
export interface ISuperSimpleQueueHelper {
|
||||||
readonly serviceName: string;
|
readonly serviceName: string;
|
||||||
@@ -39,7 +38,7 @@ export interface MonitorActionDecision {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: any;
|
private logger: any;
|
||||||
@@ -282,19 +281,18 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
|
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (monitor.type !== "http") {
|
if (!monitor.geoCheckEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!supportsGeoCheck(monitor.type)) {
|
||||||
this.logger.debug({
|
this.logger.debug({
|
||||||
message: `Monitor ${monitorId} is not HTTP type, skipping geo check`,
|
message: `Monitor ${monitorId} type does not support geo checks, skipping`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getGeoCheckJob",
|
method: "getGeoCheckJob",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!monitor.geoCheckEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
|
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `No geo check locations configured for monitor ${monitorId}`,
|
message: `No geo check locations configured for monitor ${monitorId}`,
|
||||||
@@ -414,5 +412,3 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
|||||||
return decision;
|
return decision;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SuperSimpleQueueHelper;
|
|
||||||
@@ -15,7 +15,7 @@ export interface IBufferService {
|
|||||||
flushGeoBuffer(): Promise<void>;
|
flushGeoBuffer(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class BufferService implements IBufferService {
|
export class BufferService implements IBufferService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private BUFFER_TIMEOUT: number;
|
private BUFFER_TIMEOUT: number;
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -177,5 +177,3 @@ class BufferService implements IBufferService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BufferService;
|
|
||||||
@@ -23,7 +23,7 @@ export interface IEmailService {
|
|||||||
sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise<string | false | undefined>;
|
sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise<string | false | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmailService implements IEmailService {
|
export class EmailService implements IEmailService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private settingsService: ISettingsService;
|
private settingsService: ISettingsService;
|
||||||
@@ -178,5 +178,3 @@ class EmailService implements IEmailService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EmailService;
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js";
|
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js";
|
||||||
|
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||||
|
import { MonitorType } from "@/types/index.js";
|
||||||
import type { ILogger } from "@/utils/logger.js";
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
|
|
||||||
@@ -8,7 +10,7 @@ const POLL_INTERVAL_MS = 2000;
|
|||||||
const MAX_POLL_TIMEOUT_MS = 30000;
|
const MAX_POLL_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
interface GlobalPingMeasurementRequest {
|
interface GlobalPingMeasurementRequest {
|
||||||
type: "http";
|
type: MonitorType;
|
||||||
target: string;
|
target: string;
|
||||||
locations: Array<{ continent: GeoContinent }>;
|
locations: Array<{ continent: GeoContinent }>;
|
||||||
limit: number;
|
limit: number;
|
||||||
@@ -44,17 +46,26 @@ interface GlobalPingProbeResult {
|
|||||||
firstByte: number;
|
firstByte: number;
|
||||||
download: number;
|
download: number;
|
||||||
};
|
};
|
||||||
|
stats?: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
avg: number;
|
||||||
|
total: number;
|
||||||
|
loss: number;
|
||||||
|
rcv: number;
|
||||||
|
drop: number;
|
||||||
|
};
|
||||||
rawOutput?: string;
|
rawOutput?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGlobalPingService {
|
export interface IGlobalPingService {
|
||||||
readonly serviceName: string;
|
readonly serviceName: string;
|
||||||
createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null>;
|
createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null>;
|
||||||
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
|
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GlobalPingService implements IGlobalPingService {
|
export class GlobalPingService implements IGlobalPingService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -67,13 +78,16 @@ class GlobalPingService implements IGlobalPingService {
|
|||||||
return GlobalPingService.SERVICE_NAME;
|
return GlobalPingService.SERVICE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null> {
|
async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
|
if (!supportsGeoCheck(monitorType)) {
|
||||||
|
throw new Error(`Unsupported monitor type for GlobalPing: ${monitorType}`);
|
||||||
|
}
|
||||||
// GlobalPing API expects target without protocol (http:// or https://)
|
// GlobalPing API expects target without protocol (http:// or https://)
|
||||||
const cleanTarget = url.replace(/^https?:\/\//, "");
|
const cleanTarget = url.replace(/^https?:\/\//, "");
|
||||||
|
|
||||||
const requestBody: GlobalPingMeasurementRequest = {
|
const requestBody: GlobalPingMeasurementRequest = {
|
||||||
type: "http",
|
type: monitorType,
|
||||||
target: cleanTarget,
|
target: cleanTarget,
|
||||||
locations: locations.map((continent) => ({ continent })),
|
locations: locations.map((continent) => ({ continent })),
|
||||||
limit: locations.length,
|
limit: locations.length,
|
||||||
@@ -88,10 +102,9 @@ class GlobalPingService implements IGlobalPingService {
|
|||||||
const measurementId = response.body.id;
|
const measurementId = response.body.id;
|
||||||
|
|
||||||
this.logger.debug({
|
this.logger.debug({
|
||||||
message: `Created GlobalPing measurement: ${measurementId}`,
|
message: `Created GlobalPing measurement: ${measurementId} for target: ${cleanTarget}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "createMeasurement",
|
method: "createMeasurement",
|
||||||
details: { measurementId, url, locations },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return measurementId;
|
return measurementId;
|
||||||
@@ -165,8 +178,7 @@ class GlobalPingService implements IGlobalPingService {
|
|||||||
const successfulResults: GeoCheckResult[] = [];
|
const successfulResults: GeoCheckResult[] = [];
|
||||||
|
|
||||||
for (const probeResult of probeResults) {
|
for (const probeResult of probeResults) {
|
||||||
// Skip failed or timeout results
|
if (probeResult.result.status !== "finished") {
|
||||||
if (probeResult.result.status !== "finished" || !probeResult.result.statusCode || !probeResult.result.timings) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,23 +192,38 @@ class GlobalPingService implements IGlobalPingService {
|
|||||||
latitude: probeResult.probe.latitude,
|
latitude: probeResult.probe.latitude,
|
||||||
};
|
};
|
||||||
|
|
||||||
const timings: GeoCheckTimings = {
|
// HTTP results have statusCode and timings, ping results have stats
|
||||||
total: probeResult.result.timings.total,
|
if (probeResult.result.statusCode && probeResult.result.timings) {
|
||||||
dns: probeResult.result.timings.dns,
|
const timings: GeoCheckTimings = {
|
||||||
tcp: probeResult.result.timings.tcp,
|
total: probeResult.result.timings.total,
|
||||||
tls: probeResult.result.timings.tls,
|
dns: probeResult.result.timings.dns,
|
||||||
firstByte: probeResult.result.timings.firstByte,
|
tcp: probeResult.result.timings.tcp,
|
||||||
download: probeResult.result.timings.download,
|
tls: probeResult.result.timings.tls,
|
||||||
};
|
firstByte: probeResult.result.timings.firstByte,
|
||||||
|
download: probeResult.result.timings.download,
|
||||||
|
};
|
||||||
|
|
||||||
const result: GeoCheckResult = {
|
successfulResults.push({
|
||||||
location,
|
location,
|
||||||
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
|
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
|
||||||
statusCode: probeResult.result.statusCode,
|
statusCode: probeResult.result.statusCode,
|
||||||
timings,
|
timings,
|
||||||
};
|
});
|
||||||
|
} else if (probeResult.result.stats) {
|
||||||
successfulResults.push(result);
|
successfulResults.push({
|
||||||
|
location,
|
||||||
|
status: probeResult.result.stats.loss === 0,
|
||||||
|
statusCode: probeResult.result.stats.loss === 0 ? 200 : 5000,
|
||||||
|
timings: {
|
||||||
|
total: probeResult.result.stats.avg,
|
||||||
|
dns: 0,
|
||||||
|
tcp: 0,
|
||||||
|
tls: 0,
|
||||||
|
firstByte: 0,
|
||||||
|
download: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return successfulResults;
|
return successfulResults;
|
||||||
@@ -206,5 +233,3 @@ class GlobalPingService implements IGlobalPingService {
|
|||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GlobalPingService;
|
|
||||||
@@ -4,7 +4,7 @@ import { MonitorType, Monitor } from "@/types/monitor.js";
|
|||||||
import { MonitorStatusResponse } from "@/types/network.js";
|
import { MonitorStatusResponse } from "@/types/network.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import ping from "ping";
|
import ping from "ping";
|
||||||
import { buildStatusResponse, timeRequest } from "@/service/infrastructure/network/utils.js";
|
import { timeRequest } from "@/service/infrastructure/network/utils.js";
|
||||||
const SERVICE_NAME = "PingProvider";
|
const SERVICE_NAME = "PingProvider";
|
||||||
|
|
||||||
type Ping = typeof ping;
|
type Ping = typeof ping;
|
||||||
@@ -33,7 +33,7 @@ export class PingProvider implements IStatusProvider<PingStatusPayload> {
|
|||||||
|
|
||||||
const sanitizedHost = this.sanitizeHost(monitor.url);
|
const sanitizedHost = this.sanitizeHost(monitor.url);
|
||||||
const { response, error } = await timeRequest<PingStatusPayload>(() => this.ping.promise.probe(sanitizedHost));
|
const { response, error } = await timeRequest<PingStatusPayload>(() => this.ping.promise.probe(sanitizedHost));
|
||||||
const safeTime = typeof response?.time === "number" ? response.time : parseFloat(String(response?.time)) || 0;
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -42,16 +42,18 @@ export class PingProvider implements IStatusProvider<PingStatusPayload> {
|
|||||||
throw new Error(`No response from ping for host: ${sanitizedHost}`);
|
throw new Error(`No response from ping for host: ${sanitizedHost}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildStatusResponse<PingStatusPayload>({
|
const responseTime = typeof response.time === "number" ? response.time : parseFloat(String(response.time)) || 0;
|
||||||
monitor,
|
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: response.alive ?? false,
|
||||||
|
code: response.alive ? 200 : 5000,
|
||||||
|
message: response.alive ? "Success" : "Ping failed",
|
||||||
|
responseTime,
|
||||||
payload: response,
|
payload: response,
|
||||||
overrides: {
|
};
|
||||||
status: response.alive ?? false,
|
|
||||||
code: 200,
|
|
||||||
message: "Success",
|
|
||||||
responseTime: safeTime,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
throw new AppError({
|
throw new AppError({
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ export class PortProvider implements IStatusProvider<PortStatusPayload> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log(error);
|
|
||||||
const errorMessage = error instanceof Error ? error.message : "Port check failed";
|
const errorMessage = error instanceof Error ? error.message : "Port check failed";
|
||||||
return {
|
return {
|
||||||
monitorId: monitor.id,
|
monitorId: monitor.id,
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||||
|
import { WebSocketStatusPayload, MonitorStatusResponse } from "@/types/network.js";
|
||||||
|
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js";
|
||||||
|
import type WebSocket from "ws";
|
||||||
|
|
||||||
|
type WebSocketConstructor = typeof WebSocket;
|
||||||
|
|
||||||
|
const SERVICE_NAME = "WebSocketProvider";
|
||||||
|
const TIMEOUT_MS = 10000;
|
||||||
|
|
||||||
|
export class WebSocketProvider implements IStatusProvider<WebSocketStatusPayload> {
|
||||||
|
readonly type = "websocket";
|
||||||
|
|
||||||
|
constructor(private WS: WebSocketConstructor) {}
|
||||||
|
|
||||||
|
supports(type: MonitorType): boolean {
|
||||||
|
return type === "websocket";
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(monitor: Monitor): Promise<MonitorStatusResponse<WebSocketStatusPayload>> {
|
||||||
|
try {
|
||||||
|
const { url } = monitor;
|
||||||
|
if (!url) {
|
||||||
|
throw new Error("URL is required for WebSocket monitoring");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { responseTime, error } = await timeRequest(async () => {
|
||||||
|
return new Promise<{ connected: boolean }>((resolve, reject) => {
|
||||||
|
const options: WebSocket.ClientOptions = {};
|
||||||
|
if (monitor.ignoreTlsErrors) {
|
||||||
|
options.rejectUnauthorized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = new this.WS(url, options);
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
ws.close();
|
||||||
|
reject(new Error("WebSocket connection timeout"));
|
||||||
|
}, TIMEOUT_MS);
|
||||||
|
|
||||||
|
ws.on("open", () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
ws.close();
|
||||||
|
resolve({ connected: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("error", (err: unknown) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
ws.close();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "WebSocket check failed";
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: false,
|
||||||
|
code: NETWORK_ERROR,
|
||||||
|
message: errorMessage,
|
||||||
|
responseTime: responseTime,
|
||||||
|
timings: undefined,
|
||||||
|
payload: { connected: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
status: true,
|
||||||
|
code: 200,
|
||||||
|
message: "WebSocket check successful",
|
||||||
|
responseTime: responseTime,
|
||||||
|
timings: undefined,
|
||||||
|
payload: { connected: true },
|
||||||
|
};
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new AppError({
|
||||||
|
message: originalMessage || "Error performing WebSocket check",
|
||||||
|
status: 500,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "handle",
|
||||||
|
details: { url: monitor.url },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
import { HTTPError, RequestError } from "got";
|
|
||||||
import type { Response } from "got";
|
|
||||||
import type { Monitor } from "@/types/monitor.js";
|
|
||||||
import { MonitorStatusResponse, MonitorStatusResponseOverrides } from "@/types/network.js";
|
|
||||||
|
|
||||||
export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ response: T | null; responseTime: number; error: unknown }> => {
|
export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ response: T | null; responseTime: number; error: unknown }> => {
|
||||||
const start = process.hrtime.bigint();
|
const start = process.hrtime.bigint();
|
||||||
try {
|
try {
|
||||||
@@ -17,69 +12,3 @@ export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ res
|
|||||||
|
|
||||||
export const NETWORK_ERROR = 5000;
|
export const NETWORK_ERROR = 5000;
|
||||||
export const PING_ERROR = 5001;
|
export const PING_ERROR = 5001;
|
||||||
|
|
||||||
interface BuildStatusResponseArgs<T> {
|
|
||||||
monitor: Monitor;
|
|
||||||
response?: Response<T> | null;
|
|
||||||
error?: Error | RequestError | HTTPError | null;
|
|
||||||
payload?: T | null;
|
|
||||||
jsonPath?: string;
|
|
||||||
matchMethod?: MonitorStatusResponse["matchMethod"];
|
|
||||||
expectedValue?: string;
|
|
||||||
extracted?: unknown;
|
|
||||||
overrides?: MonitorStatusResponseOverrides<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const buildStatusResponse = <T>({
|
|
||||||
monitor,
|
|
||||||
response,
|
|
||||||
error,
|
|
||||||
payload,
|
|
||||||
jsonPath,
|
|
||||||
matchMethod,
|
|
||||||
expectedValue,
|
|
||||||
extracted,
|
|
||||||
overrides,
|
|
||||||
}: BuildStatusResponseArgs<T>): MonitorStatusResponse<T> => {
|
|
||||||
if (error) {
|
|
||||||
const statusResponse: MonitorStatusResponse<T> = {
|
|
||||||
monitorId: monitor.id,
|
|
||||||
teamId: monitor.teamId,
|
|
||||||
type: monitor.type,
|
|
||||||
status: false,
|
|
||||||
code: NETWORK_ERROR,
|
|
||||||
message: error.message ?? "Network error",
|
|
||||||
responseTime: 0,
|
|
||||||
timings: undefined,
|
|
||||||
jsonPath,
|
|
||||||
matchMethod,
|
|
||||||
expectedValue,
|
|
||||||
extracted,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error instanceof HTTPError || error instanceof RequestError) {
|
|
||||||
statusResponse.code = error?.response?.statusCode ?? NETWORK_ERROR;
|
|
||||||
statusResponse.message = error.message;
|
|
||||||
statusResponse.responseTime = error.timings?.phases?.total ?? 0;
|
|
||||||
statusResponse.timings = error.timings;
|
|
||||||
}
|
|
||||||
return { ...statusResponse, ...(overrides ?? {}) };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
monitorId: monitor.id,
|
|
||||||
teamId: monitor.teamId,
|
|
||||||
type: monitor.type,
|
|
||||||
status: response?.ok ?? false,
|
|
||||||
code: response?.statusCode ?? NETWORK_ERROR,
|
|
||||||
message: response?.statusMessage ?? "",
|
|
||||||
responseTime: response?.timings?.phases?.total ?? 0,
|
|
||||||
timings: response?.timings,
|
|
||||||
payload: payload ?? response?.body,
|
|
||||||
jsonPath,
|
|
||||||
matchMethod,
|
|
||||||
expectedValue,
|
|
||||||
extracted,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,38 +1,14 @@
|
|||||||
import type {
|
import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
|
||||||
Monitor,
|
|
||||||
MonitorStatusResponse,
|
|
||||||
GrpcStatusPayload,
|
|
||||||
PageSpeedStatusPayload,
|
|
||||||
HttpStatusPayload,
|
|
||||||
HardwareStatusPayload,
|
|
||||||
PingStatusPayload,
|
|
||||||
DockerStatusPayload,
|
|
||||||
PortStatusPayload,
|
|
||||||
GameStatusPayload,
|
|
||||||
} from "@/types/index.js";
|
|
||||||
import type { AxiosStatic } from "axios";
|
import type { AxiosStatic } from "axios";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
|
||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
import { IStatusProvider } from "./network/IStatusProvider.js";
|
import { IStatusProvider } from "./network/IStatusProvider.js";
|
||||||
const SERVICE_NAME = "NetworkService";
|
const SERVICE_NAME = "NetworkService";
|
||||||
|
|
||||||
export interface INetworkService {
|
export interface INetworkService {
|
||||||
readonly serviceName: string;
|
readonly serviceName: string;
|
||||||
requestStatus(
|
requestStatus(monitor: Monitor): Promise<MonitorStatusResponse<unknown>>;
|
||||||
monitor: Monitor
|
|
||||||
): Promise<
|
|
||||||
MonitorStatusResponse<
|
|
||||||
| PingStatusPayload
|
|
||||||
| HttpStatusPayload
|
|
||||||
| PageSpeedStatusPayload
|
|
||||||
| HardwareStatusPayload
|
|
||||||
| DockerStatusPayload
|
|
||||||
| PortStatusPayload
|
|
||||||
| GameStatusPayload
|
|
||||||
| GrpcStatusPayload
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
requestWebhook(
|
requestWebhook(
|
||||||
type: string,
|
type: string,
|
||||||
url: string,
|
url: string,
|
||||||
@@ -47,66 +23,19 @@ export interface INetworkService {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NetworkService implements INetworkService {
|
export class NetworkService implements INetworkService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private TYPE_PING: string;
|
|
||||||
private TYPE_HTTP: string;
|
|
||||||
private TYPE_PAGESPEED: string;
|
|
||||||
private TYPE_HARDWARE: string;
|
|
||||||
private TYPE_DOCKER: string;
|
|
||||||
private TYPE_PORT: string;
|
|
||||||
private TYPE_GAME: string;
|
|
||||||
private TYPE_GRPC: string;
|
|
||||||
private SERVICE_NAME: string;
|
|
||||||
private NETWORK_ERROR: number;
|
|
||||||
private PING_ERROR: number;
|
|
||||||
|
|
||||||
private axios: AxiosStatic;
|
private axios: AxiosStatic;
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
private pingProvider;
|
|
||||||
private httpProvider;
|
|
||||||
private pageSpeedProvider;
|
|
||||||
private hardwareProvider;
|
|
||||||
private dockerProvider;
|
|
||||||
private portProvider;
|
|
||||||
private gameProvider;
|
|
||||||
private grpcProvider;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
axios: AxiosStatic,
|
axios: AxiosStatic,
|
||||||
logger: ILogger,
|
logger: ILogger,
|
||||||
pingProvider: IStatusProvider<PingStatusPayload>,
|
private providers: IStatusProvider<unknown>[]
|
||||||
httpProvider: IStatusProvider<HttpStatusPayload>,
|
|
||||||
pagespeedProvider: IStatusProvider<PageSpeedStatusPayload>,
|
|
||||||
hardwareProvider: IStatusProvider<HardwareStatusPayload>,
|
|
||||||
dockerProvider: IStatusProvider<DockerStatusPayload>,
|
|
||||||
portProvider: IStatusProvider<PortStatusPayload>,
|
|
||||||
gameProvider: IStatusProvider<GameStatusPayload>,
|
|
||||||
grpcProvider: IStatusProvider<GrpcStatusPayload>
|
|
||||||
) {
|
) {
|
||||||
this.TYPE_PING = "ping";
|
|
||||||
this.TYPE_HTTP = "http";
|
|
||||||
this.TYPE_PAGESPEED = "pagespeed";
|
|
||||||
this.TYPE_HARDWARE = "hardware";
|
|
||||||
this.TYPE_DOCKER = "docker";
|
|
||||||
this.TYPE_PORT = "port";
|
|
||||||
this.TYPE_GAME = "game";
|
|
||||||
this.TYPE_GRPC = "grpc";
|
|
||||||
this.SERVICE_NAME = SERVICE_NAME;
|
|
||||||
this.NETWORK_ERROR = 5000;
|
|
||||||
this.PING_ERROR = 5001;
|
|
||||||
this.axios = axios;
|
this.axios = axios;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
|
||||||
this.pingProvider = pingProvider;
|
|
||||||
this.httpProvider = httpProvider;
|
|
||||||
this.pageSpeedProvider = pagespeedProvider;
|
|
||||||
this.hardwareProvider = hardwareProvider;
|
|
||||||
this.dockerProvider = dockerProvider;
|
|
||||||
this.portProvider = portProvider;
|
|
||||||
this.gameProvider = gameProvider;
|
|
||||||
this.grpcProvider = grpcProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get serviceName(): string {
|
get serviceName(): string {
|
||||||
@@ -114,41 +43,12 @@ class NetworkService implements INetworkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Main entry point
|
// Main entry point
|
||||||
async requestStatus(
|
async requestStatus(monitor: Monitor) {
|
||||||
monitor: Monitor
|
const provider = this.providers.find((p) => p.supports(monitor.type));
|
||||||
): Promise<
|
if (!provider) {
|
||||||
MonitorStatusResponse<
|
return this.handleUnsupportedType(monitor.type);
|
||||||
| PingStatusPayload
|
|
||||||
| HttpStatusPayload
|
|
||||||
| PageSpeedStatusPayload
|
|
||||||
| HardwareStatusPayload
|
|
||||||
| DockerStatusPayload
|
|
||||||
| PortStatusPayload
|
|
||||||
| GameStatusPayload
|
|
||||||
| GrpcStatusPayload
|
|
||||||
>
|
|
||||||
> {
|
|
||||||
const type = monitor?.type || "unknown";
|
|
||||||
switch (type) {
|
|
||||||
case this.TYPE_PING:
|
|
||||||
return await this.pingProvider.handle(monitor);
|
|
||||||
case this.TYPE_HTTP:
|
|
||||||
return await this.httpProvider.handle(monitor);
|
|
||||||
case this.TYPE_PAGESPEED:
|
|
||||||
return await this.pageSpeedProvider.handle(monitor);
|
|
||||||
case this.TYPE_HARDWARE:
|
|
||||||
return await this.hardwareProvider.handle(monitor);
|
|
||||||
case this.TYPE_DOCKER:
|
|
||||||
return await this.dockerProvider.handle(monitor);
|
|
||||||
case this.TYPE_PORT:
|
|
||||||
return await this.portProvider.handle(monitor);
|
|
||||||
case this.TYPE_GAME:
|
|
||||||
return await this.gameProvider.handle(monitor);
|
|
||||||
case this.TYPE_GRPC:
|
|
||||||
return await this.grpcProvider.handle(monitor);
|
|
||||||
default:
|
|
||||||
return this.handleUnsupportedType(type);
|
|
||||||
}
|
}
|
||||||
|
return provider.handle(monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
|
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
|
||||||
@@ -157,7 +57,7 @@ class NetworkService implements INetworkService {
|
|||||||
teamId: "unknown",
|
teamId: "unknown",
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
status: false,
|
status: false,
|
||||||
code: this.NETWORK_ERROR,
|
code: NETWORK_ERROR,
|
||||||
message: `Unsupported type: ${type}`,
|
message: `Unsupported type: ${type}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -181,7 +81,7 @@ class NetworkService implements INetworkService {
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: err instanceof Error ? err.message : String(err),
|
message: err instanceof Error ? err.message : String(err),
|
||||||
service: this.SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "requestWebhook",
|
method: "requestWebhook",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,7 +90,7 @@ class NetworkService implements INetworkService {
|
|||||||
return {
|
return {
|
||||||
type: "webhook",
|
type: "webhook",
|
||||||
status: false,
|
status: false,
|
||||||
code: axiosError.response?.status ?? this.NETWORK_ERROR,
|
code: axiosError.response?.status ?? NETWORK_ERROR,
|
||||||
message: `Failed to send ${type} notification`,
|
message: `Failed to send ${type} notification`,
|
||||||
payload: axiosError.response?.data,
|
payload: axiosError.response?.data,
|
||||||
};
|
};
|
||||||
@@ -199,7 +99,7 @@ class NetworkService implements INetworkService {
|
|||||||
return {
|
return {
|
||||||
type: "webhook",
|
type: "webhook",
|
||||||
status: false,
|
status: false,
|
||||||
code: this.NETWORK_ERROR,
|
code: NETWORK_ERROR,
|
||||||
message: `Failed to send ${type} notification`,
|
message: `Failed to send ${type} notification`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -225,7 +125,7 @@ class NetworkService implements INetworkService {
|
|||||||
|
|
||||||
throw new AppError({
|
throw new AppError({
|
||||||
message: originalMessage || "Error sending PagerDuty notification",
|
message: originalMessage || "Error sending PagerDuty notification",
|
||||||
service: this.SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "requestPagerDuty",
|
method: "requestPagerDuty",
|
||||||
details: {
|
details: {
|
||||||
responseData: err && typeof err === "object" && "response" in err ? (err as { response?: { data?: unknown } }).response?.data : undefined,
|
responseData: err && typeof err === "object" && "response" in err ? (err as { response?: { data?: unknown } }).response?.data : undefined,
|
||||||
@@ -268,7 +168,7 @@ class NetworkService implements INetworkService {
|
|||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: err.message,
|
message: err.message,
|
||||||
service: this.SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "requestMatrix",
|
method: "requestMatrix",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -276,7 +176,7 @@ class NetworkService implements INetworkService {
|
|||||||
const axiosError = err as { response?: { status?: number; data?: unknown } };
|
const axiosError = err as { response?: { status?: number; data?: unknown } };
|
||||||
return {
|
return {
|
||||||
status: false,
|
status: false,
|
||||||
code: axiosError.response?.status || this.NETWORK_ERROR,
|
code: axiosError.response?.status || NETWORK_ERROR,
|
||||||
message: "Failed to send Matrix notification",
|
message: "Failed to send Matrix notification",
|
||||||
payload: axiosError.response?.data,
|
payload: axiosError.response?.data,
|
||||||
};
|
};
|
||||||
@@ -285,17 +185,15 @@ class NetworkService implements INetworkService {
|
|||||||
|
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: String(err),
|
message: String(err),
|
||||||
service: this.SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "requestMatrix",
|
method: "requestMatrix",
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: false,
|
status: false,
|
||||||
code: this.NETWORK_ERROR,
|
code: NETWORK_ERROR,
|
||||||
message: "Failed to send Matrix notification",
|
message: "Failed to send Matrix notification",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NetworkService;
|
|
||||||
@@ -21,7 +21,7 @@ export interface ISettingsService {
|
|||||||
getDBSettings(): Promise<Settings>;
|
getDBSettings(): Promise<Settings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsService implements ISettingsService {
|
export class SettingsService implements ISettingsService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private settings: EnvConfig;
|
private settings: EnvConfig;
|
||||||
private settingsRepository: ISettingsRepository;
|
private settingsRepository: ISettingsRepository;
|
||||||
@@ -74,5 +74,3 @@ class SettingsService implements ISettingsService {
|
|||||||
return settings;
|
return settings;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsService;
|
|
||||||
@@ -3,9 +3,12 @@ export type { CheckSnapshot } from "@/types/check.js";
|
|||||||
import type { GeoContinent } from "@/types/geoCheck.js";
|
import type { GeoContinent } from "@/types/geoCheck.js";
|
||||||
export type { GeoContinent } from "@/types/geoCheck.js";
|
export type { GeoContinent } from "@/types/geoCheck.js";
|
||||||
|
|
||||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "unknown"] as const;
|
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const;
|
||||||
export type MonitorType = (typeof MonitorTypes)[number];
|
export type MonitorType = (typeof MonitorTypes)[number];
|
||||||
|
|
||||||
|
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
|
||||||
|
export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type);
|
||||||
|
|
||||||
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
|
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
|
||||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export interface MonitorStatusResponse<
|
|||||||
| HardwareStatusPayload
|
| HardwareStatusPayload
|
||||||
| DockerStatusPayload
|
| DockerStatusPayload
|
||||||
| GameStatusPayload
|
| GameStatusPayload
|
||||||
| GrpcStatusPayload,
|
| GrpcStatusPayload
|
||||||
|
| WebSocketStatusPayload,
|
||||||
> {
|
> {
|
||||||
monitorId: string;
|
monitorId: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -111,6 +112,10 @@ export interface GrpcStatusPayload {
|
|||||||
servingStatus: string;
|
servingStatus: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WebSocketStatusPayload {
|
||||||
|
connected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MonitorPayloadMap {
|
export interface MonitorPayloadMap {
|
||||||
ping: PingStatusPayload;
|
ping: PingStatusPayload;
|
||||||
http: HttpStatusPayload;
|
http: HttpStatusPayload;
|
||||||
@@ -120,6 +125,7 @@ export interface MonitorPayloadMap {
|
|||||||
port: PortStatusPayload;
|
port: PortStatusPayload;
|
||||||
game: GameStatusPayload;
|
game: GameStatusPayload;
|
||||||
grpc: GrpcStatusPayload;
|
grpc: GrpcStatusPayload;
|
||||||
|
websocket: WebSocketStatusPayload;
|
||||||
default: unknown;
|
default: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const getChecksParamValidation = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getChecksQueryValidation = z.object({
|
export const getChecksQueryValidation = z.object({
|
||||||
type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc"]).optional(),
|
type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"]).optional(),
|
||||||
sortOrder: z.enum(["asc", "desc"]).optional(),
|
sortOrder: z.enum(["asc", "desc"]).optional(),
|
||||||
limit: z.coerce.number().optional(),
|
limit: z.coerce.number().optional(),
|
||||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
|
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const envSchema = z.object({
|
const envSchema = z.object({
|
||||||
// Server Configuration
|
// Server Configuration
|
||||||
|
PORT: z.string().default("52345"),
|
||||||
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
||||||
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("debug"),
|
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("debug"),
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export const getMonitorsByTeamIdParamValidation = z.object({});
|
|||||||
export const getMonitorsByTeamIdQueryValidation = z.object({
|
export const getMonitorsByTeamIdQueryValidation = z.object({
|
||||||
type: z
|
type: z
|
||||||
.union([
|
.union([
|
||||||
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]),
|
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]),
|
||||||
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"])),
|
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])),
|
||||||
])
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
filter: z.union([z.string(), z.literal(""), z.null()]).optional(),
|
filter: z.union([z.string(), z.literal(""), z.null()]).optional(),
|
||||||
@@ -37,8 +37,8 @@ export const getMonitorsWithChecksQueryValidation = z.object({
|
|||||||
order: z.enum(["asc", "desc"]).optional(),
|
order: z.enum(["asc", "desc"]).optional(),
|
||||||
type: z
|
type: z
|
||||||
.union([
|
.union([
|
||||||
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]),
|
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]),
|
||||||
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"])),
|
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])),
|
||||||
])
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
explain: booleanCoercion.optional(),
|
explain: booleanCoercion.optional(),
|
||||||
|
|||||||
Reference in new issue
Block a user