Compare commits

...
64 Commits
Author SHA1 Message Date
Alexander Holliday c2170267e8 Merge pull request #3394 from bluewave-labs/develop
develop -> master 3.5.1 release
2026-03-06 12:56:20 -08:00
Alexander Holliday ff1d473b81 Merge pull request #3392 from bluewave-labs/feat/version-bump
bump bersion
2026-03-06 12:48:48 -08:00
Alex Holliday bd384bde4a bump version 2026-03-06 12:47:00 -08:00
Alexander Holliday e7f052dfa0 Merge pull request #3386 from bluewave-labs/feat/remove-team-member
Add ability to remove team members
2026-03-06 12:37:55 -08:00
Alex Holliday 9903c72ff5 merge 2026-03-06 12:36:26 -08:00
Alex Holliday d7d237d37b move button strings to correct location, flex-end 2026-03-06 12:35:38 -08:00
Alexander Holliday 5d03f6f0f0 Merge pull request #3391 from ajhollid/fix/bump-zod
explicitly bump zod
2026-03-06 12:27:34 -08:00
Alex Holliday 38e2a64255 explicitly bump zod 2026-03-06 12:25:32 -08:00
Alexander Holliday 1f56338697 Merge pull request #3390 from bluewave-labs/fix/port-env-var
Fix: restore PORT env var
2026-03-06 12:20:02 -08:00
gorkem-bwl 172467c0a6 fix: address PR review feedback
- Use useIsAdmin and useIsSuperAdmin hooks instead of manual role checks
- Close delete dialog on failure, not just on success
2026-03-06 15:19:01 -05:00
Alexander Holliday ff2b6dff2b Merge pull request #3387 from bluewave-labs/feat/uptime-websockets
Feat: Uptime - Websockets
2026-03-06 12:12:11 -08:00
Br0wnHammer 9f600f87af Fix: Dev Dependencies in Server 2026-03-07 01:40:07 +05:30
Br0wnHammer 7eeff7a396 Fix: Dev Dependencies 2026-03-07 01:37:37 +05:30
Alex Holliday afd6a12e7d restore PORT env var 2026-03-06 12:07:18 -08:00
Br0wnHammer 125d70c4b1 Format Checks 2026-03-07 01:13:00 +05:30
Br0wnHammer 5ebe46a81c Feat: Websocket Implementation 2026-03-07 01:11:20 +05:30
Br0wnHammer 19174ab3e5 Feat: update filter controls and default types 2026-03-07 01:02:30 +05:30
gorkem-bwl 7c953978ed refactor: simplify EditUser imports and canDeleteUser logic
- Merge duplicate react-router-dom imports into one
- Replace useIsAdmin + useIsSuperAdmin + useSelector with single useSelector
- Remove redundant !includes("superadmin") check from canDeleteUser
2026-03-06 14:31:59 -05:00
Br0wnHammer db5441d0e2 Feat: Frontend - update form hook and create/edit page 2026-03-07 01:01:49 +05:30
Br0wnHammer 1d529e3be4 Feat: Frontend - Add websocket to client types and validations 2026-03-07 01:00:38 +05:30
Br0wnHammer 23247d4319 Feat: Update server validation 2026-03-07 00:59:39 +05:30
Br0wnHammer 216d9b2996 Feat: Backend - Config 2026-03-07 00:58:55 +05:30
Br0wnHammer 62f1e62a26 Feat: Backend service - Implement WebSocket check in NetworkService 2026-03-07 00:57:52 +05:30
Br0wnHammer 55bdae77f9 Feat: Backend types - Register websocket type and payload 2026-03-07 00:56:58 +05:30
gorkem-bwl 82998a7795 feat: add ability to remove team members
## Changes
- Add DELETE /api/v1/auth/users/:userId endpoint (admin + superadmin)
- Add deleteUserById service method with role hierarchy enforcement
- Superadmins can remove admins and regular users
- Admins can only remove regular users
- No one can remove superadmins, demo users, or themselves via this endpoint
- Add "Remove user" button with confirmation dialog on edit user page
- Allow admins to view user details and access the edit user page
- Add i18n strings for remove user button and confirmation dialog

## Access control
- superadmin -> can remove admin, user
- admin -> can remove user only
- admin cannot remove superadmin or other admins (enforced on both backend and frontend)
2026-03-06 14:18:27 -05:00
Alexander Holliday 9fe8b7572f Merge pull request #3374 from bluewave-labs/fix/dark-mode-background-color
Fix dark mode background to use neutral gray
2026-03-06 11:14:21 -08:00
Alex Holliday a424092fbd remove unused imports 2026-03-06 11:12:52 -08:00
Alexander Holliday ad427dc765 Merge pull request #3385 from bluewave-labs/feat/globalping-ping-type
feat: globalping ping type
2026-03-06 11:10:45 -08:00
Alex Holliday 116eb80fc9 format 2026-03-06 11:07:48 -08:00
Alex Holliday c094a5fd73 format 2026-03-06 11:02:20 -08:00
Alex Holliday 206e28d1b9 user helper 2026-03-06 11:01:38 -08:00
Alex Holliday 2f1a164c26 remove rtt 2026-03-06 10:46:00 -08:00
Alex Holliday 124a8c0b07 ping type globalping checks 2026-03-06 10:37:29 -08:00
Alexander Holliday 188445c57a Merge pull request #3384 from bluewave-labs/fix/prettier
fix prettier hook
2026-03-06 09:46:08 -08:00
Alex Holliday 35f018623f fix prettier hook 2026-03-06 09:45:33 -08:00
Alexander Holliday d64526fed2 Merge pull request #3383 from bluewave-labs/fix/providers
fix: providers
2026-03-06 09:39:14 -08:00
Alex Holliday 9a8a175f7c format 2026-03-06 09:37:45 -08:00
Alex Holliday 6ecb27d522 remove buildStatusResponse 2026-03-06 09:35:52 -08:00
Alex Holliday 24b9b64979 manually build response 2026-03-06 09:35:07 -08:00
Alexander Holliday a4e4e1313e Merge pull request #3382 from bluewave-labs/fix/ts-errors
fix: resolve ts errors
2026-03-06 09:19:02 -08:00
Alex Holliday 990dc7875a format 2026-03-06 09:16:22 -08:00
Alex Holliday 69e98a51ec resolve ts errors 2026-03-06 09:14:12 -08:00
Alexander Holliday 3063b9cf22 Merge pull request #3379 from bluewave-labs/fix/network-providers
fix: network providers
2026-03-05 14:52:33 -08:00
Alex Holliday bd32854c7a cleanup services 2026-03-05 14:46:28 -08:00
Alex Holliday 0718e63e2f cleanup network service 2026-03-05 14:11:11 -08:00
Alexander Holliday cdb70f9171 Merge pull request #3378 from bluewave-labs/fix/email
Fix/email
2026-03-05 14:01:03 -08:00
gorkem-bwl a1068f1d7e refactor: move dark background colors to colors object
Addresses review feedback — define gray900 and gray850 in the colors
object and reference them in the dark palette.
2026-03-04 19:56:54 -05:00
gorkem-bwl 75eca743b4 fix: use neutral gray background for dark mode
## Changes
- Replace MUI default blue-tinted dark background with neutral grays
  (#151518 for default, #1c1c21 for paper/cards)
- Remove decorative blue/purple SVG blob overlay from dark mode background

## Benefits
- Dark mode now has a clean, neutral gray appearance instead of a blue tint
2026-03-04 16:30:19 -05:00
Alexander Holliday 321ca59380 Merge pull request #3368 from bluewave-labs/develop
Develop -> master (3.5.0)
2026-03-04 10:41:02 -08:00
Alexander Holliday e3ecfe6ad8 Merge pull request #3323 from bluewave-labs/develop
develop -> master
2026-02-20 12:38:33 -08:00
Alexander Holliday f4664eec16 Merge pull request #3321 from bluewave-labs/develop
develop->master
2026-02-20 12:20:49 -08:00
Alexander Holliday 56f601e34b Merge pull request #3319 from bluewave-labs/develop
develop -> master
2026-02-20 12:07:29 -08:00
Alexander Holliday 1244ff6d93 Merge pull request #3212 from bluewave-labs/develop
develop -> master
2026-01-27 13:58:48 -08:00
Alexander Holliday c0ea804c98 Merge pull request #3209 from bluewave-labs/develop
develop -> master
2026-01-27 13:40:25 -08:00
Alexander Holliday c21cf29db6 Merge pull request #3207 from bluewave-labs/develop
develop -> master
2026-01-27 13:20:18 -08:00
Alexander Holliday 3d0da70ce6 Merge pull request #3204 from bluewave-labs/develop
develop -> master
2026-01-27 13:09:47 -08:00
Alexander Holliday feb061240b Merge pull request #3042 from bluewave-labs/develop
develp->master
2025-10-27 12:09:00 -07:00
Alexander Holliday a924227805 Merge pull request #2964 from bluewave-labs/develop
develop -> master
2025-09-18 13:40:45 -07:00
Alexander Holliday 2144d3f8c0 Merge pull request #2962 from bluewave-labs/develop
develop -> master
2025-09-18 12:04:51 -07:00
Alexander Holliday fcd6786149 Merge pull request #2936 from bluewave-labs/develop
develop -> master
2025-09-08 20:43:21 -07:00
Alexander Holliday ea968ebf3c Merge pull request #2933 from bluewave-labs/develop
develop -> master
2025-09-08 20:33:35 -07:00
Alexander Holliday 8a7b852974 Merge pull request #2929 from bluewave-labs/develop
develop -> master
2025-09-08 07:24:16 -07:00
Alexander Holliday c8d3e3b70c Merge pull request #2921 from bluewave-labs/develop
develop -> master
2025-09-04 15:46:30 -07:00
Alexander Holliday 7a90a6f9f4 Merge pull request #2915 from bluewave-labs/develop
develop->master
2025-09-01 10:33:09 -07:00
50 changed files with 693 additions and 521 deletions

No files matched your search

+3 -1
View File
@@ -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
View File
@@ -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"];
+8
View File
@@ -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,
+64 -11
View File
@@ -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}
/> />
); );
}; };
+28 -3
View File
@@ -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")}
+3 -3
View File
@@ -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();
+1 -1
View File
@@ -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(() => {
+10
View File
@@ -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",
+1
View File
@@ -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",
+6
View File
@@ -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,
},
}; };
+9
View File
@@ -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,
}; };
+15 -1
View File
@@ -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": {
+1 -1
View File
@@ -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: "/",
+15 -76
View File
@@ -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
View File
@@ -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",
+76 -51
View File
@@ -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,
+14
View File
@@ -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();
+4
View File
@@ -0,0 +1,4 @@
export interface IDb {
connect(): Promise<void>;
disconnect(): Promise<void>;
}
+11 -9
View File
@@ -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
View File
@@ -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;
} }
+2 -1
View File
@@ -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);
+1 -3
View File
@@ -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;
+24 -4
View File
@@ -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;
+7 -3
View File
@@ -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: [] };
} }
+97 -25
View File
@@ -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;
+25 -6
View File
@@ -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;
+1 -3
View File
@@ -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;
+4 -1
View File
@@ -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];
+7 -1
View File
@@ -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;
} }
+1 -1
View File
@@ -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(),
+1
View File
@@ -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"),
+4 -4
View File
@@ -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(),