From 75eca743b4316b36aef3ea48a3989e9d16b0c6bf Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 4 Mar 2026 16:30:19 -0500 Subject: [PATCH 01/32] 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 --- client/src/Components/layout/AppLayout.tsx | 4 ---- client/src/Utils/Theme/Palette.ts | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/Components/layout/AppLayout.tsx b/client/src/Components/layout/AppLayout.tsx index baadd85ca..369bd0726 100644 --- a/client/src/Components/layout/AppLayout.tsx +++ b/client/src/Components/layout/AppLayout.tsx @@ -47,10 +47,6 @@ const AppLayout = ({ children }: AppLayoutProps) => { sx={{ minHeight: "100vh", backgroundColor: theme.palette.background.default, - backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none", - backgroundSize: "100% 100%", - backgroundPosition: "center", - backgroundRepeat: "no-repeat", }} > diff --git a/client/src/Utils/Theme/Palette.ts b/client/src/Utils/Theme/Palette.ts index d314c8e27..01a9a704b 100644 --- a/client/src/Utils/Theme/Palette.ts +++ b/client/src/Utils/Theme/Palette.ts @@ -31,4 +31,8 @@ export const darkPalette = { secondary: { main: colors.gray700, }, + background: { + default: "#151518", + paper: "#1c1c21", + }, }; From a1068f1d7ed6648fb89bda756fa7134fd305222e Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 4 Mar 2026 19:56:54 -0500 Subject: [PATCH 02/32] refactor: move dark background colors to colors object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback — define gray900 and gray850 in the colors object and reference them in the dark palette. --- client/src/Utils/Theme/Palette.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/Utils/Theme/Palette.ts b/client/src/Utils/Theme/Palette.ts index 01a9a704b..9c19f0cbf 100644 --- a/client/src/Utils/Theme/Palette.ts +++ b/client/src/Utils/Theme/Palette.ts @@ -12,6 +12,8 @@ export const typographyLevels = { export const colors = { gray200: "#EFEFEF", gray700: "#313131", + gray900: "#151518", + gray850: "#1c1c21", blueBlueWave: "#1570EF", }; @@ -32,7 +34,7 @@ export const darkPalette = { main: colors.gray700, }, background: { - default: "#151518", - paper: "#1c1c21", + default: colors.gray900, + paper: colors.gray850, }, }; From 69e98a51ec03bde22dcb763f9d71b3c6a49df477 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:14:12 -0800 Subject: [PATCH 03/32] resolve ts errors --- server/src/service/business/userService.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index fde9d6292..7cd1491c3 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -141,7 +141,7 @@ export class UserService implements IUserService { message: "New user created", service: SERVICE_NAME, method: "registerUser", - details: newUser.id, + details: { userId: newUser.id }, }); delete newUser.profileImage; @@ -205,7 +205,7 @@ export class UserService implements IUserService { message: "New user created by superadmin", service: SERVICE_NAME, method: "createUser", - details: newUser.id, + details: { userId: newUser.id }, }); newUser.profileImage = undefined; @@ -280,6 +280,14 @@ export class UserService implements IUserService { email, 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); return msgId; }; From 990dc7875ab3ec5f9fad7bd36ca5ef2054286de4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:16:22 -0800 Subject: [PATCH 04/32] format --- server/src/service/business/checkService.ts | 1 - server/src/service/business/diagnosticService.ts | 1 - server/src/service/business/geoChecksService.ts | 1 - server/src/service/business/incidentService.ts | 1 - server/src/service/business/inviteService.ts | 1 - server/src/service/business/maintenanceWindowService.ts | 1 - .../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts | 1 - .../infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts | 1 - server/src/service/infrastructure/bufferService.ts | 1 - server/src/service/infrastructure/emailService.ts | 1 - server/src/service/infrastructure/globalPingService.ts | 1 - server/src/service/infrastructure/networkService.ts | 1 - server/src/service/system/settingsService.ts | 1 - 13 files changed, 13 deletions(-) diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 8779b0a00..c83a3d411 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -194,4 +194,3 @@ export class CheckService implements ICheckService { throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } }); }; } - diff --git a/server/src/service/business/diagnosticService.ts b/server/src/service/business/diagnosticService.ts index f7a8b8ff9..f9c53f633 100644 --- a/server/src/service/business/diagnosticService.ts +++ b/server/src/service/business/diagnosticService.ts @@ -107,4 +107,3 @@ export class DiagnosticService implements IDiagnosticService { return diagnostics; }; } - diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index 5c5869d94..4a31348b0 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -188,4 +188,3 @@ export class GeoChecksService implements IGeoChecksService { return result; }; } - diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 81a98893a..710336016 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -266,4 +266,3 @@ export class IncidentService implements IIncidentService { } }; } - diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts index 26b3b9dbe..9d1cbf59d 100644 --- a/server/src/service/business/inviteService.ts +++ b/server/src/service/business/inviteService.ts @@ -94,4 +94,3 @@ export class InviteService implements IInviteService { return await this.invitesRepository.findByToken(inviteToken); }; } - diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index 2257718f3..abf513eb5 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -96,4 +96,3 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { return await this.maintenanceWindowsRepository.updateById(id, teamId, body); }; } - diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index 216a821b1..2857f5b82 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -289,4 +289,3 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { }); }; } - diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 11a44d1bd..011008b32 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -412,4 +412,3 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { return decision; } } - diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index ae89a1c09..229c5e7d4 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -177,4 +177,3 @@ export class BufferService implements IBufferService { } } } - diff --git a/server/src/service/infrastructure/emailService.ts b/server/src/service/infrastructure/emailService.ts index 180a728f3..f536f3f18 100755 --- a/server/src/service/infrastructure/emailService.ts +++ b/server/src/service/infrastructure/emailService.ts @@ -178,4 +178,3 @@ export class EmailService implements IEmailService { } }; } - diff --git a/server/src/service/infrastructure/globalPingService.ts b/server/src/service/infrastructure/globalPingService.ts index 0a65412f4..b58486ad3 100644 --- a/server/src/service/infrastructure/globalPingService.ts +++ b/server/src/service/infrastructure/globalPingService.ts @@ -206,4 +206,3 @@ export class GlobalPingService implements IGlobalPingService { return new Promise((resolve) => setTimeout(resolve, ms)); } } - diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index a459c6480..e475bc24b 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -197,4 +197,3 @@ export class NetworkService implements INetworkService { } } } - diff --git a/server/src/service/system/settingsService.ts b/server/src/service/system/settingsService.ts index 46959c554..1d75c0a36 100755 --- a/server/src/service/system/settingsService.ts +++ b/server/src/service/system/settingsService.ts @@ -74,4 +74,3 @@ export class SettingsService implements ISettingsService { return settings; }; } - From 24b9b649791c45f085db82aa8e316183eb7c9a69 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:35:07 -0800 Subject: [PATCH 05/32] manually build response --- .../infrastructure/network/PingProvider.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/server/src/service/infrastructure/network/PingProvider.ts b/server/src/service/infrastructure/network/PingProvider.ts index 2749584f5..03181b078 100644 --- a/server/src/service/infrastructure/network/PingProvider.ts +++ b/server/src/service/infrastructure/network/PingProvider.ts @@ -4,7 +4,7 @@ import { MonitorType, Monitor } from "@/types/monitor.js"; import { MonitorStatusResponse } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; 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"; type Ping = typeof ping; @@ -33,7 +33,7 @@ export class PingProvider implements IStatusProvider { const sanitizedHost = this.sanitizeHost(monitor.url); const { response, error } = await timeRequest(() => this.ping.promise.probe(sanitizedHost)); - const safeTime = typeof response?.time === "number" ? response.time : parseFloat(String(response?.time)) || 0; + if (error) { throw error; } @@ -42,16 +42,19 @@ export class PingProvider implements IStatusProvider { throw new Error(`No response from ping for host: ${sanitizedHost}`); } - return buildStatusResponse({ - monitor, + const responseTime = + typeof response.time === "number" ? response.time : parseFloat(String(response.time)) || 0; + + 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, - overrides: { - status: response.alive ?? false, - code: 200, - message: "Success", - responseTime: safeTime, - }, - }); + }; } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); throw new AppError({ From 6ecb27d522cb1eec1ce6be7b6c395e9824a31a4e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:35:52 -0800 Subject: [PATCH 06/32] remove buildStatusResponse --- .../service/infrastructure/network/utils.ts | 71 ------------------- 1 file changed, 71 deletions(-) diff --git a/server/src/service/infrastructure/network/utils.ts b/server/src/service/infrastructure/network/utils.ts index c70f06bf8..ddb962f47 100644 --- a/server/src/service/infrastructure/network/utils.ts +++ b/server/src/service/infrastructure/network/utils.ts @@ -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 (operation: () => Promise): Promise<{ response: T | null; responseTime: number; error: unknown }> => { const start = process.hrtime.bigint(); try { @@ -17,69 +12,3 @@ export const timeRequest = async (operation: () => Promise): Promise<{ res export const NETWORK_ERROR = 5000; export const PING_ERROR = 5001; - -interface BuildStatusResponseArgs { - monitor: Monitor; - response?: Response | null; - error?: Error | RequestError | HTTPError | null; - payload?: T | null; - jsonPath?: string; - matchMethod?: MonitorStatusResponse["matchMethod"]; - expectedValue?: string; - extracted?: unknown; - overrides?: MonitorStatusResponseOverrides; -} - -export const buildStatusResponse = ({ - monitor, - response, - error, - payload, - jsonPath, - matchMethod, - expectedValue, - extracted, - overrides, -}: BuildStatusResponseArgs): MonitorStatusResponse => { - if (error) { - const statusResponse: MonitorStatusResponse = { - 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, - }; -}; From 9a8a175f7cdff86638ad0412c23505d6cf651fd6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:37:45 -0800 Subject: [PATCH 07/32] format --- server/src/service/infrastructure/network/PingProvider.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/service/infrastructure/network/PingProvider.ts b/server/src/service/infrastructure/network/PingProvider.ts index 03181b078..0b9691da9 100644 --- a/server/src/service/infrastructure/network/PingProvider.ts +++ b/server/src/service/infrastructure/network/PingProvider.ts @@ -42,8 +42,7 @@ export class PingProvider implements IStatusProvider { throw new Error(`No response from ping for host: ${sanitizedHost}`); } - const responseTime = - typeof response.time === "number" ? response.time : parseFloat(String(response.time)) || 0; + const responseTime = typeof response.time === "number" ? response.time : parseFloat(String(response.time)) || 0; return { monitorId: monitor.id, From 35f018623f22b630f39befc389e7b2efe34180de Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:45:33 -0800 Subject: [PATCH 08/32] fix prettier hook --- client/package.json | 2 +- server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/package.json b/client/package.json index ba2d59275..b6c48e1c1 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,7 @@ "format-check": "prettier --check ." }, "lint-staged": { - "**/*": "npm run format" + "**/*": "prettier --write" }, "dependencies": { "@emotion/react": "^11.13.3", diff --git a/server/package.json b/server/package.json index ce2c0822e..cb3ae3131 100755 --- a/server/package.json +++ b/server/package.json @@ -15,7 +15,7 @@ "format-check": "prettier --check ." }, "lint-staged": { - "**/*": "npm run format" + "**/*": "prettier --write" }, "keywords": [], "author": "", From 124a8c0b07ba21711e3844818fd8f84588006d00 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 10:37:29 -0800 Subject: [PATCH 09/32] ping type globalping checks --- client/src/Pages/CreateMonitor/index.tsx | 2 +- client/src/Pages/Uptime/Details/index.tsx | 12 ++- .../geo-checks/MongoGeoChecksRepository.ts | 7 +- .../src/service/business/geoChecksService.ts | 2 +- server/src/service/business/monitorService.ts | 2 +- .../SuperSimpleQueue/SuperSimpleQueue.ts | 9 ++- .../SuperSimpleQueueHelper.ts | 11 ++- .../infrastructure/globalPingService.ts | 74 +++++++++++++------ server/src/types/geoCheck.ts | 1 + 9 files changed, 80 insertions(+), 40 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 34e141795..dac5189ba 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -885,7 +885,7 @@ const CreateMonitorPage = () => { /> )} - {watchedType === "http" && ( + {(watchedType === "http" || watchedType === "ping") && ( { ); const geoChecksUrl = useMemo(() => { - if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) { + if ( + !monitorId || + (monitor?.type !== "http" && monitor?.type !== "ping") || + !monitor?.geoCheckEnabled + ) { return null; } const params = new URLSearchParams(); @@ -141,7 +145,11 @@ const UptimeDetailsPage = () => { // Fetch paginated geo checks for the table const geoChecksTableUrl = useMemo(() => { - if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) { + if ( + !monitorId || + (monitor?.type !== "http" && monitor?.type !== "ping") || + !monitor?.geoCheckEnabled + ) { return null; } const params = new URLSearchParams(); diff --git a/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts b/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts index 7633fcf7c..e1db5ab1d 100644 --- a/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts +++ b/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts @@ -1,17 +1,18 @@ import { IGeoChecksRepository } from "./IGeoChecksRepository.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 mongoose from "mongoose"; import { getDateForRange } from "@/utils/dataUtils.js"; +import { ILogger } from "@/utils/logger.js"; const SERVICE_NAME = "GeoChecksRepository"; class MongoGeoChecksRepository implements IGeoChecksRepository { static SERVICE_NAME = SERVICE_NAME; - private logger: any; - constructor(logger: any) { + private logger: ILogger; + constructor(logger: ILogger) { this.logger = logger; } diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index 4a31348b0..f7c5b33ea 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -68,7 +68,7 @@ export class GeoChecksService implements IGeoChecksService { } // 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) { // GlobalPing API is down, skip this check diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index b94d99c90..fe211c598 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -340,7 +340,7 @@ export class MonitorService implements IMonitorService { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); } - if (monitor.type !== "http" || !monitor.geoCheckEnabled) { + if ((monitor.type !== "http" && monitor.type !== "ping") || !monitor.geoCheckEnabled) { return { groupedGeoChecks: [] }; } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index 2857f5b82..3fb67bcfc 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -125,8 +125,13 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { data: monitor, }); + // Return early if we don't need geo checks + if (monitor.type !== "http" && monitor.type !== "ping") { + return; + } + // Add geo check job if enabled for HTTP monitors - if (monitor.geoCheckEnabled && monitor.type === "http") { + if (monitor.geoCheckEnabled) { this.scheduler.addJob({ id: `${monitorId}-geo`, template: "geo-check-job", @@ -175,7 +180,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { // Handle geo check job lifecycle const geoJobId = `${monitor.id}-geo`; - if (monitor.geoCheckEnabled && monitor.type === "http") { + if (monitor.geoCheckEnabled && (monitor.type === "http" || monitor.type === "ping")) { // Check if geo job exists const existingGeoJob = await this.scheduler.getJob(geoJobId); if (existingGeoJob) { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 011008b32..766f2d907 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -280,19 +280,18 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" }); } - if (monitor.type !== "http") { + if (!monitor.geoCheckEnabled) { + return; + } + if (monitor.type !== "http" && monitor.type !== "ping") { this.logger.debug({ - message: `Monitor ${monitorId} is not HTTP type, skipping geo check`, + message: `Monitor ${monitorId} is not HTTP or Ping type, skipping geo check`, service: SERVICE_NAME, method: "getGeoCheckJob", }); return; } - if (!monitor.geoCheckEnabled) { - return; - } - if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) { this.logger.warn({ message: `No geo check locations configured for monitor ${monitorId}`, diff --git a/server/src/service/infrastructure/globalPingService.ts b/server/src/service/infrastructure/globalPingService.ts index b58486ad3..ff67dfd99 100644 --- a/server/src/service/infrastructure/globalPingService.ts +++ b/server/src/service/infrastructure/globalPingService.ts @@ -1,4 +1,5 @@ import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js"; +import { MonitorType } from "@/types/index.js"; import type { ILogger } from "@/utils/logger.js"; import got from "got"; @@ -8,7 +9,7 @@ const POLL_INTERVAL_MS = 2000; const MAX_POLL_TIMEOUT_MS = 30000; interface GlobalPingMeasurementRequest { - type: "http"; + type: "http" | "ping"; target: string; locations: Array<{ continent: GeoContinent }>; limit: number; @@ -44,13 +45,22 @@ interface GlobalPingProbeResult { firstByte: number; download: number; }; + stats?: { + min: number; + max: number; + avg: number; + total: number; + loss: number; + rcv: number; + drop: number; + }; rawOutput?: string; }; } export interface IGlobalPingService { readonly serviceName: string; - createMeasurement(url: string, locations: GeoContinent[]): Promise; + createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise; pollForResults(measurementId: string, timeoutMs?: number): Promise; } @@ -67,13 +77,16 @@ export class GlobalPingService implements IGlobalPingService { return GlobalPingService.SERVICE_NAME; } - async createMeasurement(url: string, locations: GeoContinent[]): Promise { + async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise { try { + if (monitorType !== "http" && monitorType !== "ping") { + throw new Error(`Unsupported monitor type for GlobalPing: ${monitorType}`); + } // GlobalPing API expects target without protocol (http:// or https://) const cleanTarget = url.replace(/^https?:\/\//, ""); const requestBody: GlobalPingMeasurementRequest = { - type: "http", + type: monitorType, target: cleanTarget, locations: locations.map((continent) => ({ continent })), limit: locations.length, @@ -88,10 +101,9 @@ export class GlobalPingService implements IGlobalPingService { const measurementId = response.body.id; this.logger.debug({ - message: `Created GlobalPing measurement: ${measurementId}`, + message: `Created GlobalPing measurement: ${measurementId} for target: ${cleanTarget}`, service: SERVICE_NAME, method: "createMeasurement", - details: { measurementId, url, locations }, }); return measurementId; @@ -165,8 +177,7 @@ export class GlobalPingService implements IGlobalPingService { const successfulResults: GeoCheckResult[] = []; for (const probeResult of probeResults) { - // Skip failed or timeout results - if (probeResult.result.status !== "finished" || !probeResult.result.statusCode || !probeResult.result.timings) { + if (probeResult.result.status !== "finished") { continue; } @@ -180,23 +191,38 @@ export class GlobalPingService implements IGlobalPingService { latitude: probeResult.probe.latitude, }; - const timings: GeoCheckTimings = { - total: probeResult.result.timings.total, - dns: probeResult.result.timings.dns, - tcp: probeResult.result.timings.tcp, - tls: probeResult.result.timings.tls, - firstByte: probeResult.result.timings.firstByte, - download: probeResult.result.timings.download, - }; + // HTTP results have statusCode and timings, ping results have stats + if (probeResult.result.statusCode && probeResult.result.timings) { + const timings: GeoCheckTimings = { + total: probeResult.result.timings.total, + dns: probeResult.result.timings.dns, + tcp: probeResult.result.timings.tcp, + tls: probeResult.result.timings.tls, + firstByte: probeResult.result.timings.firstByte, + download: probeResult.result.timings.download, + }; - const result: GeoCheckResult = { - location, - status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300, - statusCode: probeResult.result.statusCode, - timings, - }; - - successfulResults.push(result); + successfulResults.push({ + location, + status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300, + statusCode: probeResult.result.statusCode, + timings, + }); + } else if (probeResult.result.stats) { + 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; diff --git a/server/src/types/geoCheck.ts b/server/src/types/geoCheck.ts index 548c4c65f..6a75592a9 100644 --- a/server/src/types/geoCheck.ts +++ b/server/src/types/geoCheck.ts @@ -33,6 +33,7 @@ export interface GeoCheckResult { status: boolean; statusCode: number; timings: GeoCheckTimings; + } export interface GeoCheck { From 2f1a164c26d3d139e5e31e54c57e08e9cdfe9626 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 10:46:00 -0800 Subject: [PATCH 10/32] remove rtt --- client/src/Pages/Uptime/Details/index.tsx | 14 +++----------- client/src/Types/Monitor.ts | 9 +++++++++ .../src/service/business/geoChecksService.ts | 19 +++---------------- server/src/service/business/monitorService.ts | 3 ++- .../SuperSimpleQueue/SuperSimpleQueue.ts | 5 +++-- .../SuperSimpleQueueHelper.ts | 5 +++-- .../infrastructure/globalPingService.ts | 5 +++-- server/src/types/monitor.ts | 3 +++ 8 files changed, 29 insertions(+), 34 deletions(-) diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index bea41705f..8828aa29a 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -20,7 +20,7 @@ import { useState, useMemo } from "react"; import { useParams } from "react-router-dom"; import { useSelector } from "react-redux"; 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 { GeoChecksResult, @@ -122,11 +122,7 @@ const UptimeDetailsPage = () => { ); const geoChecksUrl = useMemo(() => { - if ( - !monitorId || - (monitor?.type !== "http" && monitor?.type !== "ping") || - !monitor?.geoCheckEnabled - ) { + if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) { return null; } const params = new URLSearchParams(); @@ -145,11 +141,7 @@ const UptimeDetailsPage = () => { // Fetch paginated geo checks for the table const geoChecksTableUrl = useMemo(() => { - if ( - !monitorId || - (monitor?.type !== "http" && monitor?.type !== "ping") || - !monitor?.geoCheckEnabled - ) { + if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) { return null; } const params = new URLSearchParams(); diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 01cae62e9..b87e65bc6 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -16,6 +16,15 @@ export const MonitorTypes = [ ] as const; 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 = [ "up", "down", diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index f7c5b33ea..f35826a8f 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -76,7 +76,6 @@ export class GeoChecksService implements IGeoChecksService { message: "Skipping geo check due to API unavailability", service: SERVICE_NAME, method: "buildGeoCheck", - details: { monitorId: monitor.id }, }); return null; } @@ -90,7 +89,6 @@ export class GeoChecksService implements IGeoChecksService { message: "No successful geo check results", service: SERVICE_NAME, method: "buildGeoCheck", - details: { monitorId: monitor.id, measurementId }, }); return null; } @@ -102,17 +100,16 @@ export class GeoChecksService implements IGeoChecksService { message: `Geo check completed for monitor ${monitor.id}`, service: SERVICE_NAME, method: "buildGeoCheck", - details: { monitorId: monitor.id, resultsCount: results.length }, }); return geoCheck; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ message: "Error executing geo check", service: SERVICE_NAME, method: "buildGeoCheck", - details: { monitorId: monitor.id, error: error.message }, - stack: error.stack, + details: { monitorId: monitor.id, error: error instanceof Error ? error.message : "Unknown error" }, + stack: error instanceof Error ? error.stack : undefined, }); return null; } @@ -172,19 +169,9 @@ export class GeoChecksService implements IGeoChecksService { const { sortOrder, dateRange, page, rowsPerPage, continent } = query; 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 parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; - const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents); - return result; }; } diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index fe211c598..b197c04bd 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -8,6 +8,7 @@ import type { PageSpeedDetailsResult, GamesMap, } from "@/types/monitor.js"; +import { supportsGeoCheck } from "@/types/monitor.js"; import type { GeoContinent } from "@/types/geoCheck.js"; import type { IChecksRepository, @@ -340,7 +341,7 @@ export class MonitorService implements IMonitorService { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); } - if ((monitor.type !== "http" && monitor.type !== "ping") || !monitor.geoCheckEnabled) { + if (!supportsGeoCheck(monitor.type) || !monitor.geoCheckEnabled) { return { groupedGeoChecks: [] }; } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index 3fb67bcfc..a96e7855b 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -2,6 +2,7 @@ import { IMonitorsRepository } from "@/repositories/index.js"; import { ILogger } from "@/utils/logger.js"; import Scheduler from "super-simple-scheduler"; import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import { supportsGeoCheck } from "@/types/monitor.js"; const SERVICE_NAME = "JobQueue"; type QueueJobFailure = { @@ -126,7 +127,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { }); // Return early if we don't need geo checks - if (monitor.type !== "http" && monitor.type !== "ping") { + if (!supportsGeoCheck(monitor.type)) { return; } @@ -180,7 +181,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { // Handle geo check job lifecycle const geoJobId = `${monitor.id}-geo`; - if (monitor.geoCheckEnabled && (monitor.type === "http" || monitor.type === "ping")) { + if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) { // Check if geo job exists const existingGeoJob = await this.scheduler.getJob(geoJobId); if (existingGeoJob) { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 766f2d907..eef0a75be 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,5 +1,6 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; +import { supportsGeoCheck } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js"; import type { StatusChangeResult } from "@/types/index.js"; @@ -283,9 +284,9 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { if (!monitor.geoCheckEnabled) { return; } - if (monitor.type !== "http" && monitor.type !== "ping") { + if (!supportsGeoCheck(monitor.type)) { this.logger.debug({ - message: `Monitor ${monitorId} is not HTTP or Ping type, skipping geo check`, + message: `Monitor ${monitorId} type does not support geo checks, skipping`, service: SERVICE_NAME, method: "getGeoCheckJob", }); diff --git a/server/src/service/infrastructure/globalPingService.ts b/server/src/service/infrastructure/globalPingService.ts index ff67dfd99..41f96b286 100644 --- a/server/src/service/infrastructure/globalPingService.ts +++ b/server/src/service/infrastructure/globalPingService.ts @@ -1,4 +1,5 @@ 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 got from "got"; @@ -9,7 +10,7 @@ const POLL_INTERVAL_MS = 2000; const MAX_POLL_TIMEOUT_MS = 30000; interface GlobalPingMeasurementRequest { - type: "http" | "ping"; + type: MonitorType; target: string; locations: Array<{ continent: GeoContinent }>; limit: number; @@ -79,7 +80,7 @@ export class GlobalPingService implements IGlobalPingService { async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise { try { - if (monitorType !== "http" && monitorType !== "ping") { + if (!supportsGeoCheck(monitorType)) { throw new Error(`Unsupported monitor type for GlobalPing: ${monitorType}`); } // GlobalPing API expects target without protocol (http:// or https://) diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index a6ecc4ef4..efbaa9632 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -6,6 +6,9 @@ export type { GeoContinent } from "@/types/geoCheck.js"; export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "unknown"] as const; 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 type MonitorStatus = (typeof MonitorStatuses)[number]; From 206e28d1b999b456f23a77fa4b5e1d71d0190f3e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 11:01:38 -0800 Subject: [PATCH 11/32] user helper --- client/src/Pages/CreateMonitor/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index dac5189ba..9a310f3b1 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -32,7 +32,7 @@ import { import { SPACING, LAYOUT } from "@/Utils/Theme/constants"; import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi"; 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 { MonitorFormData } from "@/Validation/monitor"; @@ -885,7 +885,7 @@ const CreateMonitorPage = () => { /> )} - {(watchedType === "http" || watchedType === "ping") && ( + {supportsGeoCheck(watchedType) && ( Date: Fri, 6 Mar 2026 11:02:20 -0800 Subject: [PATCH 12/32] format --- client/src/Pages/CreateMonitor/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 9a310f3b1..22719986e 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -32,7 +32,12 @@ import { import { SPACING, LAYOUT } from "@/Utils/Theme/constants"; import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; -import { type Monitor, type MonitorType, type GamesMap, supportsGeoCheck } from "@/Types/Monitor"; +import { + type Monitor, + type MonitorType, + type GamesMap, + supportsGeoCheck, +} from "@/Types/Monitor"; import type { Notification } from "@/Types/Notification"; import type { MonitorFormData } from "@/Validation/monitor"; From 116eb80fc9ab485dc1593fc5a1f74a1105e0d35a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 11:07:48 -0800 Subject: [PATCH 13/32] format --- server/src/types/geoCheck.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/types/geoCheck.ts b/server/src/types/geoCheck.ts index 6a75592a9..548c4c65f 100644 --- a/server/src/types/geoCheck.ts +++ b/server/src/types/geoCheck.ts @@ -33,7 +33,6 @@ export interface GeoCheckResult { status: boolean; statusCode: number; timings: GeoCheckTimings; - } export interface GeoCheck { From a424092fbd144a4d4450719d810cfcea79aee83d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 11:12:52 -0800 Subject: [PATCH 14/32] remove unused imports --- client/src/Components/layout/AppLayout.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/src/Components/layout/AppLayout.tsx b/client/src/Components/layout/AppLayout.tsx index 369bd0726..34822c5ae 100644 --- a/client/src/Components/layout/AppLayout.tsx +++ b/client/src/Components/layout/AppLayout.tsx @@ -1,9 +1,6 @@ import Box from "@mui/material/Box"; import { useState, useEffect, useRef } from "react"; 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 { setServerUnreachableCallback, get } from "@/Utils/ApiClient"; @@ -13,7 +10,6 @@ interface AppLayoutProps { const AppLayout = ({ children }: AppLayoutProps) => { const theme = useTheme(); - const mode = useSelector((state: RootState) => state.ui.mode); const [serverUnreachable, setServerUnreachable] = useState(false); const retryIntervalRef = useRef(null); From 82998a7795b15f4ecf0aba2355f440edd0f5a3e3 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 6 Mar 2026 14:18:27 -0500 Subject: [PATCH 15/32] 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) --- client/src/Pages/Account/EditUser/index.tsx | 74 ++++++++++++++++--- .../Pages/Account/components/TeamTable.tsx | 8 +- client/src/locales/en.json | 9 +++ server/src/controllers/authController.ts | 14 ++++ server/src/routes/authRoute.ts | 3 +- server/src/service/business/userService.ts | 45 ++++++++++- 6 files changed, 136 insertions(+), 17 deletions(-) diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index a3ddebaba..482a2f7bb 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -14,9 +14,14 @@ import type { UserRole, User } from "@/Types/User"; import { useParams } from "react-router-dom"; import { useTheme } from "@mui/material"; 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 { useEffect } from "react"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { DialogInput } from "@/Components/inputs/Dialog"; +import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; interface RoleOption { id: UserRole; @@ -29,6 +34,12 @@ const EditUserPage = () => { const { userId } = useParams<{ userId: string }>(); const { resolver, defaults } = useEditUserForm(); const { patch, loading: isSaving } = usePatch(); + const { deleteFn, loading: isDeleting } = useDelete(); + const navigate = useNavigate(); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const isAdmin = useIsAdmin(); + const isSuperAdmin = useIsSuperAdmin(); + const currentUser = useSelector((state: RootState) => state.auth.user); const { data: user, isLoading } = useGet(`/auth/users/${userId}`); @@ -60,6 +71,21 @@ const EditUserPage = () => { setValue("role", newRoles, { shouldValidate: true }); }; + const canDeleteUser = + isAdmin && + userId !== currentUser?.id && + !user?.role?.includes("superadmin") && + !user?.role?.includes("demo") && + (isSuperAdmin || user?.role?.every((r) => r !== "admin" && r !== "superadmin")); + + const handleDeleteUser = async () => { + const result = await deleteFn(`/auth/users/${userId}`); + if (result) { + setShowDeleteDialog(false); + navigate("/account", { state: { tab: "team" } }); + } + }; + const onSubmit = async (data: EditUserFormData) => { await patch(`/auth/users/${userId}`, data); }; @@ -165,17 +191,45 @@ const EditUserPage = () => { } /> - + {canDeleteUser && ( + + )} + + + setShowDeleteDialog(false)} + onConfirm={handleDeleteUser} + confirmColor="error" + confirmText={t("pages.editUser.buttons.removeUser")} + loading={isDeleting} + /> ); }; diff --git a/client/src/Pages/Account/components/TeamTable.tsx b/client/src/Pages/Account/components/TeamTable.tsx index ab8dcc3b4..289bfb331 100644 --- a/client/src/Pages/Account/components/TeamTable.tsx +++ b/client/src/Pages/Account/components/TeamTable.tsx @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Table } from "@/Components/design-elements"; import type { Header } from "@/Components/design-elements/Table"; -import { useIsSuperAdmin } from "@/Hooks/useIsAdmin"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; import type { User } from "@/Types/User"; interface TeamTableProps { @@ -13,7 +13,7 @@ interface TeamTableProps { export const TeamTable = ({ users }: TeamTableProps) => { const { t } = useTranslation(); const navigate = useNavigate(); - const isSuperAdmin = useIsSuperAdmin(); + const isAdmin = useIsAdmin(); const headers: Header[] = [ { @@ -45,7 +45,7 @@ export const TeamTable = ({ users }: TeamTableProps) => { ]; const handleRowClick = (row: User) => { - if (isSuperAdmin) { + if (isAdmin) { navigate(`/account/team/${row.id}`); } }; @@ -54,7 +54,7 @@ export const TeamTable = ({ users }: TeamTableProps) => { ); }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index c6bf41478..a6d5a8d41 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -619,6 +619,15 @@ "title": "Roles", "description": "Assign roles to the user. Multiple roles can be selected." } + }, + "buttons": { + "removeUser": "Remove user" + }, + "dialog": { + "removeUser": { + "title": "Remove user", + "content": "Are you sure you want to remove {{name}} from your team? This action cannot be undone." + } } }, "incidents": { diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index 8add39087..f196e4dd5 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -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) => { try { const allUsers = await this.userService.getAllUsers(); diff --git a/server/src/routes/authRoute.ts b/server/src/routes/authRoute.ts index cc1ec3fd6..2e5fa1069 100755 --- a/server/src/routes/authRoute.ts +++ b/server/src/routes/authRoute.ts @@ -26,9 +26,10 @@ class AuthRoutes { 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.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/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.delete("/user", verifyJWT, this.authController.deleteUser); diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 7cd1491c3..48f3a51cf 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -29,6 +29,7 @@ export interface IUserService { validateRecovery(recoveryToken: string): Promise; resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>; deleteUser(user: User): Promise; + deleteUserById(actor: User, targetUserId: string): Promise; getAllUsers(): Promise; getUserById(roles: UserRole[], userId: string): Promise; editUserById(userId: string, patch: Partial): Promise; @@ -357,13 +358,53 @@ export class UserService implements IUserService { 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 () => { return await this.usersRepository.findAll(); }; getUserById = async (roles: any, userId: any) => { - if (!roles.includes("superadmin")) { - throw new AppError({ message: "User is not a superadmin", service: SERVICE_NAME, status: 403 }); + if (!roles.includes("superadmin") && !roles.includes("admin")) { + throw new AppError({ message: "Insufficient permissions", service: SERVICE_NAME, status: 403 }); } const user = await this.usersRepository.findById(userId); From 55bdae77f965a2bf025834f8591e560fc6c01d85 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 00:56:58 +0530 Subject: [PATCH 16/32] Feat: Backend types - Register websocket type and payload --- client/src/Types/Monitor.ts | 1 + server/package-lock.json | 90 ++++++------------------------------- server/package.json | 2 + server/src/types/monitor.ts | 2 +- server/src/types/network.ts | 8 +++- 5 files changed, 25 insertions(+), 78 deletions(-) diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index b87e65bc6..053b517d1 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -12,6 +12,7 @@ export const MonitorTypes = [ "port", "game", "grpc", + "websocket", "unknown", ] as const; export type MonitorType = (typeof MonitorTypes)[number]; diff --git a/server/package-lock.json b/server/package-lock.json index 889c08f15..4b751858f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,6 +12,7 @@ "@grpc/grpc-js": "^1.14.3", "@grpc/proto-loader": "^0.8.0", "@pulsecron/pulse": "1.6.8", + "@types/ws": "^8.18.1", "axios": "^1.7.2", "bcryptjs": "3.0.2", "bullmq": "5.41.2", @@ -43,6 +44,7 @@ "super-simple-scheduler": "1.4.5", "swagger-ui-express": "5.0.1", "winston": "^3.13.0", + "ws": "^8.19.0", "zod": "^4.3.6" }, "devDependencies": { @@ -871,7 +873,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1501,7 +1502,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1546,7 +1546,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -4338,7 +4337,6 @@ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4499,7 +4497,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4665,6 +4662,15 @@ "@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==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -4727,7 +4733,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -5341,7 +5346,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5920,7 +5924,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6863,7 +6866,6 @@ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz", "integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==", "license": "MIT", - "peer": true, "dependencies": { "cssnano-preset-default": "^7.0.9", "lilconfig": "^3.1.3" @@ -7631,7 +7633,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7993,7 +7994,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -9638,7 +9638,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -12509,7 +12508,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14443,63 +14441,6 @@ "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": { "version": "5.30.0", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz", @@ -14662,7 +14603,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14831,7 +14771,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14994,7 +14933,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15628,9 +15566,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/server/package.json b/server/package.json index cb3ae3131..17ed45295 100755 --- a/server/package.json +++ b/server/package.json @@ -27,6 +27,7 @@ "@grpc/grpc-js": "^1.14.3", "@grpc/proto-loader": "^0.8.0", "@pulsecron/pulse": "1.6.8", + "@types/ws": "^8.18.1", "axios": "^1.7.2", "bcryptjs": "3.0.2", "bullmq": "5.41.2", @@ -58,6 +59,7 @@ "super-simple-scheduler": "1.4.5", "swagger-ui-express": "5.0.1", "winston": "^3.13.0", + "ws": "^8.19.0", "zod": "^4.3.6" }, "devDependencies": { diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index efbaa9632..89740162b 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -3,7 +3,7 @@ export type { CheckSnapshot } from "@/types/check.js"; import 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 const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const; diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 93ddc4ac9..07e7c3736 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -24,7 +24,8 @@ export interface MonitorStatusResponse< | HardwareStatusPayload | DockerStatusPayload | GameStatusPayload - | GrpcStatusPayload, + | GrpcStatusPayload + | WebSocketStatusPayload, > { monitorId: string; teamId: string; @@ -111,6 +112,10 @@ export interface GrpcStatusPayload { servingStatus: string; } +export interface WebSocketStatusPayload { + connected: boolean; +} + export interface MonitorPayloadMap { ping: PingStatusPayload; http: HttpStatusPayload; @@ -120,6 +125,7 @@ export interface MonitorPayloadMap { port: PortStatusPayload; game: GameStatusPayload; grpc: GrpcStatusPayload; + websocket: WebSocketStatusPayload; default: unknown; } From 62f1e62a26f29a0b8c9f05a1512b488f457f5452 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 00:57:52 +0530 Subject: [PATCH 17/32] Feat: Backend service - Implement WebSocket check in NetworkService --- .../network/WebSocketProvider.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 server/src/service/infrastructure/network/WebSocketProvider.ts diff --git a/server/src/service/infrastructure/network/WebSocketProvider.ts b/server/src/service/infrastructure/network/WebSocketProvider.ts new file mode 100644 index 000000000..0af436067 --- /dev/null +++ b/server/src/service/infrastructure/network/WebSocketProvider.ts @@ -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 { + readonly type = "websocket"; + + constructor(private WS: WebSocketConstructor) {} + + supports(type: MonitorType): boolean { + return type === "websocket"; + } + + async handle(monitor: Monitor): Promise> { + 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 }, + }); + } + } +} From 216d9b2996dedf91708ae9cf61044f51b92b4295 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 00:58:55 +0530 Subject: [PATCH 18/32] Feat: Backend - Config --- server/src/config/services.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index b55bd0f87..65377eeb4 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -58,6 +58,7 @@ import { DockerProvider } from "@/service/infrastructure/network/DockerProvider. import { PortProvider } from "@/service/infrastructure/network/PortProvider.js"; import { GameProvider } from "@/service/infrastructure/network/GameProvider.js"; import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js"; +import { WebSocketProvider } from "@/service/infrastructure/network/WebSocketProvider.js"; // Third-party import axios from "axios"; @@ -77,6 +78,7 @@ import { games, GameDig } from "gamedig"; import jmespath from "jmespath"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; +import WebSocket from "ws"; // Repositories import { @@ -185,6 +187,7 @@ export const initializeServices = async ({ const portProvider = new PortProvider(net); const gameProvider = new GameProvider(logger, GameDig); const grpcProvider = new GrpcProvider(grpc, protoLoader); + const webSocketProvider = new WebSocketProvider(WebSocket); const networkService = new NetworkService(axios, logger, [ pingProvider, @@ -195,6 +198,7 @@ export const initializeServices = async ({ portProvider, gameProvider, grpcProvider, + webSocketProvider, ]); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); From 23247d43194f06af6cce5d6c988e08a47de484dd Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 00:59:39 +0530 Subject: [PATCH 19/32] Feat: Update server validation --- server/src/validation/checkValidation.ts | 2 +- server/src/validation/monitorValidation.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/validation/checkValidation.ts b/server/src/validation/checkValidation.ts index 547281d68..d4f75f3ac 100644 --- a/server/src/validation/checkValidation.ts +++ b/server/src/validation/checkValidation.ts @@ -11,7 +11,7 @@ export const getChecksParamValidation = 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(), limit: z.coerce.number().optional(), dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index 4694143ac..75ffccd19 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -21,8 +21,8 @@ export const getMonitorsByTeamIdParamValidation = z.object({}); export const getMonitorsByTeamIdQueryValidation = z.object({ type: z .union([ - z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]), - z.array(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", "websocket"])), ]) .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(), type: z .union([ - z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]), - z.array(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", "websocket"])), ]) .optional(), explain: booleanCoercion.optional(), From 1d529e3be492d23bca138e6ba795fa60c74decc9 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:00:38 +0530 Subject: [PATCH 20/32] Feat: Frontend - Add websocket to client types and validations --- client/src/Utils/MonitorUtils.ts | 1 + client/src/Validation/monitor.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts index ad744ba6d..d0ede67e1 100644 --- a/client/src/Utils/MonitorUtils.ts +++ b/client/src/Utils/MonitorUtils.ts @@ -9,6 +9,7 @@ export const getMonitorPath = (type: MonitorType): string => { ping: "uptime", game: "uptime", grpc: "uptime", + websocket: "uptime", unknown: "uptime", docker: "uptime", hardware: "infrastructure", diff --git a/client/src/Validation/monitor.ts b/client/src/Validation/monitor.ts index 439af977f..9acffe6fe 100644 --- a/client/src/Validation/monitor.ts +++ b/client/src/Validation/monitor.ts @@ -115,6 +115,13 @@ const hardwareSchema = baseSchema.extend({ 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 export const monitorSchema = z.discriminatedUnion("type", [ httpSchema, @@ -125,6 +132,7 @@ export const monitorSchema = z.discriminatedUnion("type", [ grpcSchema, pagespeedSchema, hardwareSchema, + websocketSchema, ]); export type MonitorFormData = z.infer; @@ -139,4 +147,5 @@ export { grpcSchema, pagespeedSchema, hardwareSchema, + websocketSchema, }; From db5441d0e21c133709a779e1fb8cbc3a313c7d63 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:01:49 +0530 Subject: [PATCH 21/32] Feat: Frontend - update form hook and create/edit page --- client/src/Hooks/useMonitorForm.ts | 8 ++++++++ client/src/Pages/CreateMonitor/index.tsx | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index 8b5252879..963409fc8 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -103,6 +103,14 @@ export const useMonitorForm = ({ selectedDisks: data?.selectedDisks || [], }; break; + case "websocket": + defaults = { + ...base, + type: "websocket", + url: data?.url || "", + ignoreTlsErrors: data?.ignoreTlsErrors || false, + }; + break; default: defaults = { ...base, diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 22719986e..dd68fee7d 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -146,6 +146,17 @@ const getGeneralSettingsConfig = ( showGrpcServiceName: 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; }; @@ -331,6 +342,13 @@ const CreateMonitorPage = () => { "pages.createMonitor.form.type.optionGrpcDescription" )} /> + )} @@ -747,7 +765,7 @@ const CreateMonitorPage = () => { } /> - {(watchedType === "http" || watchedType === "grpc") && ( + {(watchedType === "http" || watchedType === "grpc" || watchedType === "websocket") && ( Date: Fri, 6 Mar 2026 14:31:59 -0500 Subject: [PATCH 22/32] 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 --- client/src/Pages/Account/EditUser/index.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index 482a2f7bb..d31722dcf 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -11,15 +11,13 @@ import { ConfigBox, BasePage } from "@/Components/design-elements"; import { UserRoles } from "@/Types/User"; import { useTranslation } from "react-i18next"; 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 { useEditUserForm } from "@/Hooks/useEditUserForm"; import { useGet, usePatch, useDelete } from "@/Hooks/UseApi"; import type { EditUserFormData } from "@/Validation/editUser"; import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; import { DialogInput } from "@/Components/inputs/Dialog"; -import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; @@ -37,9 +35,9 @@ const EditUserPage = () => { const { deleteFn, loading: isDeleting } = useDelete(); const navigate = useNavigate(); const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const isAdmin = useIsAdmin(); - const isSuperAdmin = useIsSuperAdmin(); const currentUser = useSelector((state: RootState) => state.auth.user); + const isSuperAdmin = currentUser?.role?.includes("superadmin") ?? false; + const isAdmin = isSuperAdmin || (currentUser?.role?.includes("admin") ?? false); const { data: user, isLoading } = useGet(`/auth/users/${userId}`); @@ -74,7 +72,6 @@ const EditUserPage = () => { const canDeleteUser = isAdmin && userId !== currentUser?.id && - !user?.role?.includes("superadmin") && !user?.role?.includes("demo") && (isSuperAdmin || user?.role?.every((r) => r !== "admin" && r !== "superadmin")); From 19174ab3e5771216da9e31cf3c0b1228d571ef1b Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:02:30 +0530 Subject: [PATCH 23/32] Feat: update filter controls and default types --- client/src/Components/monitors/ControlsFilter.tsx | 3 ++- client/src/Pages/Uptime/Monitors/index.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/Components/monitors/ControlsFilter.tsx b/client/src/Components/monitors/ControlsFilter.tsx index 0f26e04a7..a617d454c 100644 --- a/client/src/Components/monitors/ControlsFilter.tsx +++ b/client/src/Components/monitors/ControlsFilter.tsx @@ -7,7 +7,7 @@ import type { MonitorType } from "@/Types/Monitor"; import { Typography, useTheme } from "@mui/material"; 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 = { http: "HTTP", ping: "Ping", @@ -15,6 +15,7 @@ const typeDisplayNames: Record = { docker: "Docker", game: "Game", grpc: "gRPC", + websocket: "WebSocket", }; const statuses = ["up", "down"]; const states = ["active", "paused"]; diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 80ee8e2d9..792e0fd78 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -70,7 +70,7 @@ const UptimeMonitorsPage = () => { const effectiveTypes = selectedTypes.length > 0 ? selectedTypes - : ["http", "ping", "docker", "port", "game", "grpc"]; + : ["http", "ping", "docker", "port", "game", "grpc", "websocket"]; // Build URL for monitors with checks const monitorsWithChecksUrl = useMemo(() => { From 5ebe46a81ce3e85f1a12b40b73d128dcb917dd9c Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:11:20 +0530 Subject: [PATCH 24/32] Feat: Websocket Implementation --- client/package-lock.json | 23 ++----------------- client/src/locales/en.json | 7 ++++++ server/src/service/business/monitorService.ts | 3 ++- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index e1dfadad5..b82e9e6a6 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -83,7 +83,6 @@ "node_modules/@babel/core": { "version": "7.29.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -354,7 +353,6 @@ "node_modules/@emotion/react": { "version": "11.14.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -392,7 +390,6 @@ "node_modules/@emotion/styled": { "version": "11.14.1", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1162,7 +1159,6 @@ "node_modules/@mui/material": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.7", @@ -1267,7 +1263,6 @@ "node_modules/@mui/system": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.7", @@ -2308,7 +2303,6 @@ "version": "24.5.2", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2324,7 +2318,6 @@ "node_modules/@types/react": { "version": "18.3.27", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2446,7 +2439,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2771,7 +2763,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3203,8 +3194,7 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -3586,7 +3576,6 @@ "version": "8.57.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4331,7 +4320,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5115,7 +5103,6 @@ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz", "integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -5763,7 +5750,6 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5775,7 +5761,6 @@ "node_modules/react-hook-form": { "version": "7.71.1", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -5975,8 +5960,7 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-persist": { "version": "6.0.0", @@ -6126,7 +6110,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6915,7 +6898,6 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7051,7 +7033,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/client/src/locales/en.json b/client/src/locales/en.json index c6bf41478..441a02b80 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -487,6 +487,10 @@ "grpcServiceName": { "label": "Service name", "placeholder": "e.g. my.service.v1 (leave empty for overall health)" + }, + "wsUrl": { + "label": "WebSocket URL", + "placeholder": "wss://example.com/socket" } }, "title": "General settings", @@ -498,6 +502,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.", "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.", + "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." } }, @@ -534,6 +539,8 @@ "optionGameDescription": "Monitor if a specific game server is online.", "optionGrpc": "gRPC", "optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.", + "optionWebSocket": "WebSocket", + "optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.", "optionHttp": "HTTP(S)", "optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.", "optionPing": "Ping", diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index b197c04bd..a7a30111c 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -227,7 +227,8 @@ export class MonitorService implements IMonitorService { checksData.monitorType !== "docker" && checksData.monitorType !== "port" && 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 }); } From 125d70c4b1ba121066f02bc7c956e11539ef40d9 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:13:00 +0530 Subject: [PATCH 25/32] Format Checks --- client/src/Pages/CreateMonitor/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index dd68fee7d..b80cddaaa 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -765,7 +765,9 @@ const CreateMonitorPage = () => { } /> - {(watchedType === "http" || watchedType === "grpc" || watchedType === "websocket") && ( + {(watchedType === "http" || + watchedType === "grpc" || + watchedType === "websocket") && ( Date: Fri, 6 Mar 2026 12:07:18 -0800 Subject: [PATCH 26/32] restore PORT env var --- server/src/index.ts | 5 ++--- server/src/validation/envValidation.ts | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 1a6195d80..2286d3887 100755 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -50,9 +50,8 @@ const startApp = async () => { openApiSpec, }); - const port = 52345; - const server = app.listen(port, () => { - logger.info({ message: `Server started on port:${port}` }); + const server = app.listen(env.PORT, () => { + logger.info({ message: `Server started on port:${env.PORT}` }); }); initShutdownListener(server, services); diff --git a/server/src/validation/envValidation.ts b/server/src/validation/envValidation.ts index 6dffdd0cd..2b0e36fcf 100644 --- a/server/src/validation/envValidation.ts +++ b/server/src/validation/envValidation.ts @@ -2,6 +2,7 @@ import { z } from "zod"; const envSchema = z.object({ // Server Configuration + PORT: z.string().default("52345"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("debug"), From 7eeff7a39619b6400b44a4462b35d7c2497bcd57 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:37:37 +0530 Subject: [PATCH 27/32] Fix: Dev Dependencies --- package-lock.json | 28 ++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 29 insertions(+) diff --git a/package-lock.json b/package-lock.json index 0ca809f88..cebf0bf46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,30 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { + "@types/ws": "^8.18.1", "husky": "^9.1.7" } }, + "node_modules/@types/node": { + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "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/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -27,6 +48,13 @@ "funding": { "url": "https://github.com/sponsors/typicode" } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index f5368f28c..37297e8e1 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "homepage": "https://github.com/bluewave-labs/Checkmate#readme", "devDependencies": { + "@types/ws": "^8.18.1", "husky": "^9.1.7" } } From 9f600f87af3b44e51bbb12b0d5b0bb0fbd2e0cd5 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:40:07 +0530 Subject: [PATCH 28/32] Fix: Dev Dependencies in Server --- package-lock.json | 28 ---------------------------- package.json | 1 - server/package-lock.json | 3 ++- server/package.json | 2 +- 4 files changed, 3 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index cebf0bf46..0ca809f88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,30 +9,9 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@types/ws": "^8.18.1", "husky": "^9.1.7" } }, - "node_modules/@types/node": { - "version": "25.3.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", - "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "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/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -48,13 +27,6 @@ "funding": { "url": "https://github.com/sponsors/typicode" } - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" } } } diff --git a/package.json b/package.json index 37297e8e1..f5368f28c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ }, "homepage": "https://github.com/bluewave-labs/Checkmate#readme", "devDependencies": { - "@types/ws": "^8.18.1", "husky": "^9.1.7" } } diff --git a/server/package-lock.json b/server/package-lock.json index 4b751858f..5d9d920ee 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,7 +12,6 @@ "@grpc/grpc-js": "^1.14.3", "@grpc/proto-loader": "^0.8.0", "@pulsecron/pulse": "1.6.8", - "@types/ws": "^8.18.1", "axios": "^1.7.2", "bcryptjs": "3.0.2", "bullmq": "5.41.2", @@ -65,6 +64,7 @@ "@types/papaparse": "^5.5.2", "@types/ping": "0.4.4", "@types/swagger-ui-express": "4.1.8", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", "c8": "10.1.3", @@ -4666,6 +4666,7 @@ "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": "*" diff --git a/server/package.json b/server/package.json index 17ed45295..7bceee766 100755 --- a/server/package.json +++ b/server/package.json @@ -27,7 +27,6 @@ "@grpc/grpc-js": "^1.14.3", "@grpc/proto-loader": "^0.8.0", "@pulsecron/pulse": "1.6.8", - "@types/ws": "^8.18.1", "axios": "^1.7.2", "bcryptjs": "3.0.2", "bullmq": "5.41.2", @@ -80,6 +79,7 @@ "@types/papaparse": "^5.5.2", "@types/ping": "0.4.4", "@types/swagger-ui-express": "4.1.8", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", "c8": "10.1.3", From 172467c0a6a1e100b0346cc8adfc61695373bcdd Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 6 Mar 2026 15:19:01 -0500 Subject: [PATCH 29/32] fix: address PR review feedback - Use useIsAdmin and useIsSuperAdmin hooks instead of manual role checks - Close delete dialog on failure, not just on success --- client/src/Pages/Account/EditUser/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index d31722dcf..728f9f0e6 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -20,6 +20,7 @@ import { useEffect, useState } from "react"; import { DialogInput } from "@/Components/inputs/Dialog"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; +import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin"; interface RoleOption { id: UserRole; @@ -36,8 +37,8 @@ const EditUserPage = () => { const navigate = useNavigate(); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const currentUser = useSelector((state: RootState) => state.auth.user); - const isSuperAdmin = currentUser?.role?.includes("superadmin") ?? false; - const isAdmin = isSuperAdmin || (currentUser?.role?.includes("admin") ?? false); + const isSuperAdmin = useIsSuperAdmin(); + const isAdmin = useIsAdmin(); const { data: user, isLoading } = useGet(`/auth/users/${userId}`); @@ -77,8 +78,8 @@ const EditUserPage = () => { const handleDeleteUser = async () => { const result = await deleteFn(`/auth/users/${userId}`); + setShowDeleteDialog(false); if (result) { - setShowDeleteDialog(false); navigate("/account", { state: { tab: "team" } }); } }; From 38e2a64255bf899306e7103c8e119db3e50c2ff8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 12:25:32 -0800 Subject: [PATCH 30/32] explicitly bump zod --- client/package-lock.json | 27 ++++++++++++++++++++++++--- client/package.json | 2 +- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index b82e9e6a6..9cf2824ae 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -44,7 +44,7 @@ "redux-persist": "6.0.0", "swr": "^2.3.6", "vite-plugin-svgr": "^4.2.0", - "zod": "^4.1.11" + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "24.5.2", @@ -83,6 +83,7 @@ "node_modules/@babel/core": { "version": "7.29.0", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -353,6 +354,7 @@ "node_modules/@emotion/react": { "version": "11.14.0", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -390,6 +392,7 @@ "node_modules/@emotion/styled": { "version": "11.14.1", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1159,6 +1162,7 @@ "node_modules/@mui/material": { "version": "7.3.7", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.7", @@ -1263,6 +1267,7 @@ "node_modules/@mui/system": { "version": "7.3.7", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.7", @@ -2303,6 +2308,7 @@ "version": "24.5.2", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2318,6 +2324,7 @@ "node_modules/@types/react": { "version": "18.3.27", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2439,6 +2446,7 @@ "version": "8.15.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2763,6 +2771,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3194,7 +3203,8 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -3576,6 +3586,7 @@ "version": "8.57.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4320,6 +4331,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5103,6 +5115,7 @@ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz", "integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -5750,6 +5763,7 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5761,6 +5775,7 @@ "node_modules/react-hook-form": { "version": "7.71.1", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -5960,7 +5975,8 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-persist": { "version": "6.0.0", @@ -6110,6 +6126,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6898,6 +6915,7 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7033,6 +7051,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -7340,6 +7359,8 @@ }, "node_modules/zod": { "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/client/package.json b/client/package.json index b6c48e1c1..2e6b5cfcb 100644 --- a/client/package.json +++ b/client/package.json @@ -52,7 +52,7 @@ "redux-persist": "6.0.0", "swr": "^2.3.6", "vite-plugin-svgr": "^4.2.0", - "zod": "^4.1.11" + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "24.5.2", From d7d237d37b64af677a1683919b8481777522c60f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 12:35:38 -0800 Subject: [PATCH 31/32] move button strings to correct location, flex-end --- client/src/Pages/Account/EditUser/index.tsx | 9 +++++---- client/src/locales/en.json | 6 ++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index d31722dcf..79068fac2 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -20,6 +20,7 @@ import { useEffect, useState } from "react"; import { DialogInput } from "@/Components/inputs/Dialog"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; +import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; interface RoleOption { id: UserRole; @@ -189,8 +190,9 @@ const EditUserPage = () => { } /> {canDeleteUser && ( @@ -200,7 +202,7 @@ const EditUserPage = () => { onClick={() => setShowDeleteDialog(true)} sx={{ minWidth: 100 }} > - {t("pages.editUser.buttons.removeUser")} + {t("common.buttons.removeUser")} )}