Compare commits

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

## Benefits
- Dark mode now has a clean, neutral gray appearance instead of a blue tint
2026-03-04 16:30:19 -05:00
33 changed files with 373 additions and 412 deletions

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@
"format-check": "prettier --check ."
},
"lint-staged": {
"**/*": "npm run format"
"**/*": "prettier --write"
},
"dependencies": {
"@emotion/react": "^11.13.3",
@@ -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<number | null>(null);
@@ -47,10 +43,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",
}}
>
<OfflineBanner visible={serverUnreachable} />
+7 -2
View File
@@ -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, 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 +890,7 @@ const CreateMonitorPage = () => {
/>
)}
{watchedType === "http" && (
{supportsGeoCheck(watchedType) && (
<ConfigBox
title={t("pages.createMonitor.form.geoChecks.title")}
subtitle={t("pages.createMonitor.form.geoChecks.description")}
+3 -3
View File
@@ -20,7 +20,7 @@ import { useState, useMemo } from "react";
import { useParams } from "react-router-dom";
import { 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,7 +122,7 @@ const UptimeDetailsPage = () => {
);
const geoChecksUrl = useMemo(() => {
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
return null;
}
const params = new URLSearchParams();
@@ -141,7 +141,7 @@ const UptimeDetailsPage = () => {
// Fetch paginated geo checks for the table
const geoChecksTableUrl = useMemo(() => {
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
return null;
}
const params = new URLSearchParams();
+9
View File
@@ -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",
+6
View File
@@ -12,6 +12,8 @@ export const typographyLevels = {
export const colors = {
gray200: "#EFEFEF",
gray700: "#313131",
gray900: "#151518",
gray850: "#1c1c21",
blueBlueWave: "#1570EF",
};
@@ -31,4 +33,8 @@ export const darkPalette = {
secondary: {
main: colors.gray700,
},
background: {
default: colors.gray900,
paper: colors.gray850,
},
};
+1 -1
View File
@@ -15,7 +15,7 @@
"format-check": "prettier --check ."
},
"lint-staged": {
"**/*": "npm run format"
"**/*": "prettier --write"
},
"keywords": [],
"author": "",
+72 -51
View File
@@ -1,31 +1,65 @@
import MongoDB from "../db/MongoDB.js";
import NetworkService from "../service/infrastructure/networkService.js";
import EmailService from "../service/infrastructure/emailService.js";
import BufferService from "../service/infrastructure/bufferService.js";
import { IDb } from "@/db/IDb.js";
import {
// Service classes
NetworkService,
EmailService,
BufferService,
GlobalPingService,
SuperSimpleQueue,
SuperSimpleQueueHelper,
NotificationsService,
StatusService,
NotificationMessageBuilder,
MonitorService,
StatusPageService,
UserService,
CheckService,
GeoChecksService,
DiagnosticService,
InviteService,
MaintenanceWindowService,
IncidentService,
// Notification providers
WebhookProvider,
SlackProvider,
EmailProvider,
DiscordProvider,
PagerDutyProvider,
MatrixProvider,
// Interfaces
INetworkService,
IEmailService,
IBufferService,
ISuperSimpleQueue,
INotificationsService,
IStatusService,
IMonitorService,
IUserService,
ICheckService,
IGeoChecksService,
IDiagnosticService,
IInviteService,
IMaintenanceWindowService,
IStatusPageService,
IIncidentService,
INotificationMessageBuilder,
ISettingsService,
EnvConfig,
} from "@/service/index.js";
import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import UserService from "../service/business/userService.js";
import CheckService from "../service/business/checkService.js";
import GeoChecksService from "../service/business/geoChecksService.js";
import GlobalPingService from "../service/infrastructure/globalPingService.js";
import DiagnosticService from "../service/business/diagnosticService.js";
import InviteService from "../service/business/inviteService.js";
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
import { MonitorService } from "@/service/index.js";
import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js";
import IncidentService from "../service/business/incidentService.js";
import { NotificationMessageBuilder, INotificationMessageBuilder } from "../service/infrastructure/notificationMessageBuilder.js";
// Network providers
import { PingProvider } from "@/service/infrastructure/network/PingProvider.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js";
import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js";
import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js";
import { PortProvider } from "@/service/infrastructure/network/PortProvider.js";
import { GameProvider } from "@/service/infrastructure/network/GameProvider.js";
import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js";
// Third-party
import axios from "axios";
import got from "got";
import ping from "ping";
@@ -44,7 +78,7 @@ import jmespath from "jmespath";
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
// repositories
// Repositories
import {
MongoMonitorsRepository,
MongoChecksRepository,
@@ -73,33 +107,23 @@ import {
IMaintenanceWindowsRepository,
} from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js";
import { PingProvider } from "@/service/infrastructure/network/PingProvider.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js";
import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js";
import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js";
import { PortProvider } from "@/service/infrastructure/network/PortProvider.js";
import { GameProvider } from "@/service/infrastructure/network/GameProvider.js";
import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js";
export type InitializedServices = {
settingsService: any;
db: any;
networkService: any;
emailService: any;
bufferService: any;
statusService: any;
jobQueue: any;
userService: any;
checkService: any;
geoChecksService: any;
diagnosticService: any;
inviteService: any;
maintenanceWindowService: any;
monitorService: any;
incidentService: any;
settingsService: ISettingsService;
db: IDb;
networkService: INetworkService;
emailService: IEmailService;
bufferService: IBufferService;
statusService: IStatusService;
jobQueue: ISuperSimpleQueue;
userService: IUserService;
checkService: ICheckService;
geoChecksService: IGeoChecksService;
diagnosticService: IDiagnosticService;
inviteService: IInviteService;
maintenanceWindowService: IMaintenanceWindowService;
monitorService: IMonitorService;
incidentService: IIncidentService;
logger: ILogger;
notificationsService: INotificationsService;
statusPageService: IStatusPageService;
@@ -129,7 +153,7 @@ export const initializeServices = async ({
}: {
logger: ILogger;
envSettings: EnvConfig;
settingsService: any;
settingsService: ISettingsService;
settingsRepository: ISettingsRepository;
}): Promise<InitializedServices> => {
// Create DB
@@ -152,8 +176,7 @@ export const initializeServices = async ({
const teamsRepository = new MongoTeamsRepository();
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
// Providers
// Network providers
const pingProvider = new PingProvider(ping);
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath));
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger);
@@ -163,9 +186,7 @@ export const initializeServices = async ({
const gameProvider = new GameProvider(logger, GameDig);
const grpcProvider = new GrpcProvider(grpc, protoLoader);
const networkService = new NetworkService(
axios,
logger,
const networkService = new NetworkService(axios, logger, [
pingProvider,
httpProvider,
pageSpeedProvider,
@@ -173,8 +194,8 @@ export const initializeServices = async ({
dockerProvider,
portProvider,
gameProvider,
grpcProvider
);
grpcProvider,
]);
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
const notificationMessageBuilder = new NotificationMessageBuilder();
@@ -196,6 +217,7 @@ export const initializeServices = async ({
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
// Notification providers
const webhookProvider = new WebhookProvider(logger);
const slackProvider = new SlackProvider(logger);
const emailProvider = new EmailProvider(emailService, logger);
@@ -279,7 +301,6 @@ export const initializeServices = async ({
const statusPageService = new StatusPageService(statusPagesRepository);
const services = {
//v1
settingsService,
db,
networkService,
+4
View File
@@ -0,0 +1,4 @@
export interface IDb {
connect(): Promise<void>;
disconnect(): Promise<void>;
}
+11 -9
View File
@@ -1,10 +1,12 @@
import mongoose from "mongoose";
import AppSettings from "./models/AppSettings.js";
import { runMigrations } from "./migration/index.js";
import AppSettings from "@/db/models/AppSettings.js";
import { runMigrations } from "@/db/migration/index.js";
import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js";
const SERVICE_NAME = "MongoDB";
class MongoDB {
import { IDb } from "@/db/IDb.js";
class MongoDB implements IDb {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
@@ -46,12 +48,12 @@ class MongoDB {
});
await runMigrations();
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: error.message,
message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME,
method: "connect",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
}
@@ -63,12 +65,12 @@ class MongoDB {
await mongoose.disconnect();
this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" });
return;
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: error.message,
message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME,
method: "disconnect",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
}
};
+3 -4
View File
@@ -9,7 +9,7 @@ import fs from "fs";
import { runMigrations } from "./db/migration/index.js";
import Logger, { ILogger } from "@/utils/logger.js";
import SettingsService from "@/service/system/settingsService.js";
import { SettingsService } from "@/service/index.js";
import { MongoSettingsRepository } from "./repositories/index.js";
const SERVICE_NAME = "Server";
@@ -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);
@@ -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;
}
+1 -3
View File
@@ -19,7 +19,7 @@ export interface ICheckService {
updateChecksTTL(params: { teamId: string; ttl: string }): Promise<void>;
}
class CheckService implements ICheckService {
export class CheckService implements ICheckService {
static SERVICE_NAME = SERVICE_NAME;
private monitorsRepository: IMonitorsRepository;
@@ -194,5 +194,3 @@ class CheckService implements ICheckService {
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } });
};
}
export default CheckService;
@@ -3,15 +3,19 @@ import os from "os";
const SERVICE_NAME = "diagnosticService";
interface MemoryUsageMetrics {
rss: number;
heapTotal: number;
heapUsed: number;
external: number;
arrayBuffers: number;
export interface IDiagnosticService {
getCPUUsage(): Promise<{ userUsageMs: number; systemUsageMs: number; usagePercentage: number }>;
getSystemStats(): Promise<{
osStats: { freeMemoryBytes: number; totalMemoryBytes: number };
memoryUsage: Record<keyof NodeJS.MemoryUsage, number>;
cpuUsage: { userUsageMs: number; systemUsageMs: number; usagePercentage: number };
v8HeapStats: { totalHeapSizeBytes: number; usedHeapSizeBytes: number; heapSizeLimitBytes: number };
eventLoopDelayMs: number;
uptimeMs: number;
}>;
}
class DiagnosticService {
export class DiagnosticService implements IDiagnosticService {
static SERVICE_NAME = SERVICE_NAME;
constructor() {
@@ -103,5 +107,3 @@ class DiagnosticService {
return diagnostics;
};
}
export default DiagnosticService;
@@ -16,7 +16,7 @@ export interface IGeoChecksService {
getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise<any>;
}
class GeoChecksService implements IGeoChecksService {
export class GeoChecksService implements IGeoChecksService {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
@@ -68,7 +68,7 @@ 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
@@ -76,7 +76,6 @@ 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 @@ 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 @@ 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,21 +169,9 @@ 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;
};
}
export default GeoChecksService;
+24 -4
View File
@@ -5,12 +5,34 @@ import { AppError } from "@/utils/AppError.js";
import { ParseBoolean } from "@/utils/utils.js";
import { getDateForRange } from "@/utils/dataUtils.js";
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
import type { Incident } from "@/types/index.js";
import type { Incident, IncidentSummary, User } from "@/types/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
import type { ILogger } from "@/utils/logger.js";
class IncidentService {
export interface IIncidentService {
handleIncident(
monitor: Monitor,
code: number,
decision: MonitorActionDecision,
monitorStatusResponse?: MonitorStatusResponse
): Promise<Incident | null>;
resolveIncident(incidentId: string, userId: string, teamId: string, comment?: string, userEmail?: string): Promise<Incident>;
getIncidentsByTeam(
teamId: string,
sortOrder: string,
dateRange: string,
page: string,
rowsPerPage: string,
status: string,
monitorId: string,
resolutionType: string
): Promise<{ incidents: Incident[]; count: number }>;
getIncidentSummary(teamId: string, limit?: string): Promise<IncidentSummary>;
getIncidentById(incidentId: string, teamId: string): Promise<{ incident: Incident; monitor: Monitor; user: User | null }>;
}
export class IncidentService implements IIncidentService {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
@@ -244,5 +266,3 @@ class IncidentService {
}
};
}
export default IncidentService;
+7 -3
View File
@@ -5,7 +5,13 @@ import { AppError } from "@/utils/AppError.js";
const SERVICE_NAME = "inviteService";
class InviteService {
export interface IInviteService {
getInviteToken(params: { invite: Partial<Invite>; teamId: string; userRoles: UserRole[] }): Promise<Invite>;
sendInviteEmail(params: { invite: Partial<Invite>; firstName: string; userRoles: UserRole[] }): Promise<void>;
verifyInviteToken(params: { inviteToken: string }): Promise<Invite>;
}
export class InviteService implements IInviteService {
static SERVICE_NAME = SERVICE_NAME;
private settingsService: any;
@@ -88,5 +94,3 @@ class InviteService {
return await this.invitesRepository.findByToken(inviteToken);
};
}
export default InviteService;
@@ -1,10 +1,23 @@
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
import type { MaintenanceWindow } from "@/types/index.js";
import { ParseBoolean } from "@/utils/utils.js";
import { AppError } from "@/utils/AppError.js";
const SERVICE_NAME = "maintenanceWindowService";
class MaintenanceWindowService {
export interface IMaintenanceWindowService {
createMaintenanceWindow(params: { teamId: string; body: Partial<MaintenanceWindow> & { monitors: string[] } }): Promise<void>;
getMaintenanceWindowById(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
getMaintenanceWindowsByTeamId(params: {
teamId: string;
query: Record<string, string>;
}): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>;
getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise<MaintenanceWindow[]>;
deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
editMaintenanceWindow(params: { id: string; teamId: string; body: Partial<MaintenanceWindow> }): Promise<MaintenanceWindow>;
}
export class MaintenanceWindowService implements IMaintenanceWindowService {
static SERVICE_NAME = SERVICE_NAME;
private monitorsRepository: IMonitorsRepository;
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
@@ -83,5 +96,3 @@ class MaintenanceWindowService {
return await this.maintenanceWindowsRepository.updateById(id, teamId, body);
};
}
export default MaintenanceWindowService;
@@ -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.geoCheckEnabled) {
if (!supportsGeoCheck(monitor.type) || !monitor.geoCheckEnabled) {
return { groupedGeoChecks: [] };
}
+54 -23
View File
@@ -11,10 +11,31 @@ import { canManageRole, type UserRole } from "@/types/user.js";
import bcrypt from "bcryptjs";
import { AppError } from "@/utils/AppError.js";
import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import { IEmailService } from "@/service/infrastructure/emailService.js";
import { ISettingsService } from "@/service/system/settingsService.js";
import { ILogger } from "@/utils/logger.js";
import jwt from "jsonwebtoken";
type JwtType = typeof jwt;
const SERVICE_NAME = "userService";
class UserService {
export interface IUserService {
issueToken(payload: Partial<User>, appSettings: { jwtTTL?: string; jwtSecret?: string }): string;
registerUser(user: Partial<User>, inviteToken: string, file: unknown): Promise<{ user: User; token: string }>;
createUser(userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: unknown): Promise<User>;
loginUser(email: string, password: string): Promise<{ user: User; token: string }>;
editUser(updates: Partial<User & { newPassword?: string }>, file: unknown, currentUser: User): Promise<User>;
checkSuperadminExists(): Promise<boolean>;
requestRecovery(email: string): Promise<string | false | undefined>;
validateRecovery(recoveryToken: string): Promise<void>;
resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>;
deleteUser(user: User): Promise<void>;
getAllUsers(): Promise<User[]>;
getUserById(roles: UserRole[], userId: string): Promise<User>;
editUserById(userId: string, patch: Partial<User>): Promise<void>;
setPasswordByUserId(userId: string, password: string): Promise<User>;
}
export class UserService implements IUserService {
static SERVICE_NAME = SERVICE_NAME;
private hashPassword = (password: string): string => {
@@ -22,10 +43,10 @@ class UserService {
return bcrypt.hashSync(password, salt);
};
private emailService: any;
private settingsService: any;
private logger: any;
private jwt: any;
private emailService: IEmailService;
private settingsService: ISettingsService;
private logger: ILogger;
private jwt: JwtType;
private jobQueue: ISuperSimpleQueue;
private crypto: any;
private monitorsRepository: IMonitorsRepository;
@@ -50,10 +71,10 @@ class UserService {
teamsRepository,
}: {
crypto: any;
emailService: any;
settingsService: any;
logger: any;
jwt: any;
emailService: IEmailService;
settingsService: ISettingsService;
logger: ILogger;
jwt: JwtType;
jobQueue: ISuperSimpleQueue;
monitorsRepository: IMonitorsRepository;
usersRepository: IUsersRepository;
@@ -120,7 +141,7 @@ class UserService {
message: "New user created",
service: SERVICE_NAME,
method: "registerUser",
details: newUser.id,
details: { userId: newUser.id },
});
delete newUser.profileImage;
@@ -134,20 +155,23 @@ class UserService {
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
name: newUser.firstName,
});
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: any) => {
if (!html) {
throw new Error("Failed to build welcome email HTML");
}
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: unknown) => {
this.logger.warn({
message: error.message,
message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
});
} catch (error: any) {
} catch (error: unknown) {
this.logger.warn({
message: error.message,
message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
}
@@ -181,7 +205,7 @@ class UserService {
message: "New user created by superadmin",
service: SERVICE_NAME,
method: "createUser",
details: newUser.id,
details: { userId: newUser.id },
});
newUser.profileImage = undefined;
@@ -256,6 +280,14 @@ class UserService {
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;
};
@@ -313,13 +345,13 @@ class UserService {
if (roles.includes("superadmin")) {
// 2. Remove all jobs, delete checks and alerts
res &&
res?.length > 0 &&
(await Promise.all(
if (res && res.length > 0) {
await Promise.all(
res.map(async (monitor) => {
await this.jobQueue.deleteJob(monitor.id);
})
));
);
}
}
// 6. Delete the user by id
await this.usersRepository.deleteById(userId);
@@ -348,4 +380,3 @@ class UserService {
return updatedUser;
};
}
export default UserService;
+25 -6
View File
@@ -1,14 +1,33 @@
// Business services
export * from "@/service/business/checkService.js";
export * from "@/service/business/diagnosticService.js";
export * from "@/service/business/geoChecksService.js";
export * from "@/service/business/incidentService.js";
export * from "@/service/business/inviteService.js";
export * from "@/service/business/maintenanceWindowService.js";
export * from "@/service/business/monitorService.js";
export * from "@/service/business/statusPageService.js";
export * from "@/service/business/userService.js";
// Infrastructure services
export * from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
export * from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
export * from "@/service/infrastructure/notificationMessageBuilder.js";
export * from "@/service/infrastructure/bufferService.js";
export * from "@/service/infrastructure/emailService.js";
export * from "@/service/infrastructure/globalPingService.js";
export * from "@/service/infrastructure/networkService.js";
export * from "@/service/infrastructure/notificationsService.js";
export * from "@/service/infrastructure/statusService.js";
// Notification providers
export * from "@/service/infrastructure/notificationProviders/discord.js";
export * from "@/service/infrastructure/notificationProviders/email.js";
export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js";
export * from "@/service/infrastructure/notificationProviders/pagerduty.js";
export * from "@/service/infrastructure/notificationProviders/matrix.js";
export * from "@/service/infrastructure/notificationProviders/pagerduty.js";
export * from "@/service/infrastructure/notificationProviders/slack.js";
export * from "@/service/infrastructure/notificationProviders/webhook.js";
export * from "@/service/infrastructure/notificationsService.js";
export * from "@/service/infrastructure/statusService.js";
export * from "@/service/infrastructure/notificationProviders/webhook.js";
export * from "@/service/infrastructure/notificationProviders/matrix.js";
export * from "@/service/business/statusPageService.js";
// System services
export * from "@/service/system/settingsService.js";
@@ -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 = {
@@ -53,7 +54,7 @@ export interface ISuperSimpleQueue {
obliterate(): Promise<void>;
}
class SuperSimpleQueue implements ISuperSimpleQueue {
export class SuperSimpleQueue implements ISuperSimpleQueue {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
@@ -125,8 +126,13 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
data: monitor,
});
// Return early if we don't need geo checks
if (!supportsGeoCheck(monitor.type)) {
return;
}
// Add geo check job if enabled for HTTP monitors
if (monitor.geoCheckEnabled && monitor.type === "http") {
if (monitor.geoCheckEnabled) {
this.scheduler.addJob({
id: `${monitorId}-geo`,
template: "geo-check-job",
@@ -175,7 +181,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
// Handle geo check job lifecycle
const geoJobId = `${monitor.id}-geo`;
if (monitor.geoCheckEnabled && monitor.type === "http") {
if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) {
// Check if geo job exists
const existingGeoJob = await this.scheduler.getJob(geoJobId);
if (existingGeoJob) {
@@ -289,5 +295,3 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
});
};
}
export default SuperSimpleQueue;
@@ -1,10 +1,9 @@
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 } from "@/service/index.js";
import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js";
import type { StatusChangeResult } from "@/types/index.js";
import IncidentService from "@/service/business/incidentService.js";
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
import {
IMaintenanceWindowsRepository,
IMonitorsRepository,
@@ -15,7 +14,7 @@ import {
IGeoChecksRepository,
} from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js";
import { IBufferService } from "../bufferService.js";
import { IBufferService } from "@/service/index.js";
export interface ISuperSimpleQueueHelper {
readonly serviceName: string;
@@ -39,7 +38,7 @@ export interface MonitorActionDecision {
};
}
class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
static SERVICE_NAME = SERVICE_NAME;
private logger: any;
@@ -282,19 +281,18 @@ 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 (!supportsGeoCheck(monitor.type)) {
this.logger.debug({
message: `Monitor ${monitorId} is not HTTP type, skipping geo check`,
message: `Monitor ${monitorId} type does not support geo checks, skipping`,
service: SERVICE_NAME,
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}`,
@@ -414,5 +412,3 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
return decision;
}
}
export default SuperSimpleQueueHelper;
@@ -15,7 +15,7 @@ export interface IBufferService {
flushGeoBuffer(): Promise<void>;
}
class BufferService implements IBufferService {
export class BufferService implements IBufferService {
static SERVICE_NAME = SERVICE_NAME;
private BUFFER_TIMEOUT: number;
private logger: ILogger;
@@ -177,5 +177,3 @@ class BufferService implements IBufferService {
}
}
}
export default BufferService;
@@ -23,7 +23,7 @@ export interface IEmailService {
sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise<string | false | undefined>;
}
class EmailService implements IEmailService {
export class EmailService implements IEmailService {
static SERVICE_NAME = SERVICE_NAME;
private settingsService: ISettingsService;
@@ -178,5 +178,3 @@ class EmailService implements IEmailService {
}
};
}
export default EmailService;
@@ -1,4 +1,6 @@
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js";
import { supportsGeoCheck } from "@/types/monitor.js";
import { MonitorType } from "@/types/index.js";
import type { ILogger } from "@/utils/logger.js";
import got from "got";
@@ -8,7 +10,7 @@ const POLL_INTERVAL_MS = 2000;
const MAX_POLL_TIMEOUT_MS = 30000;
interface GlobalPingMeasurementRequest {
type: "http";
type: MonitorType;
target: string;
locations: Array<{ continent: GeoContinent }>;
limit: number;
@@ -44,17 +46,26 @@ 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<string | null>;
createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null>;
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
}
class GlobalPingService implements IGlobalPingService {
export class GlobalPingService implements IGlobalPingService {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
@@ -67,13 +78,16 @@ class GlobalPingService implements IGlobalPingService {
return GlobalPingService.SERVICE_NAME;
}
async createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null> {
async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null> {
try {
if (!supportsGeoCheck(monitorType)) {
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 +102,9 @@ 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 +178,7 @@ 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 +192,38 @@ 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;
@@ -206,5 +233,3 @@ class GlobalPingService implements IGlobalPingService {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
export default GlobalPingService;
@@ -4,7 +4,7 @@ import { MonitorType, Monitor } from "@/types/monitor.js";
import { MonitorStatusResponse } from "@/types/network.js";
import { 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<PingStatusPayload> {
const sanitizedHost = this.sanitizeHost(monitor.url);
const { response, error } = await timeRequest<PingStatusPayload>(() => this.ping.promise.probe(sanitizedHost));
const safeTime = typeof response?.time === "number" ? response.time : parseFloat(String(response?.time)) || 0;
if (error) {
throw error;
}
@@ -42,16 +42,18 @@ export class PingProvider implements IStatusProvider<PingStatusPayload> {
throw new Error(`No response from ping for host: ${sanitizedHost}`);
}
return buildStatusResponse<PingStatusPayload>({
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({
@@ -51,7 +51,6 @@ export class PortProvider implements IStatusProvider<PortStatusPayload> {
});
});
if (error) {
console.log(error);
const errorMessage = error instanceof Error ? error.message : "Port check failed";
return {
monitorId: monitor.id,
@@ -1,8 +1,3 @@
import { HTTPError, RequestError } from "got";
import type { Response } from "got";
import type { Monitor } from "@/types/monitor.js";
import { MonitorStatusResponse, MonitorStatusResponseOverrides } from "@/types/network.js";
export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ response: T | null; responseTime: number; error: unknown }> => {
const start = process.hrtime.bigint();
try {
@@ -17,69 +12,3 @@ export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ res
export const NETWORK_ERROR = 5000;
export const PING_ERROR = 5001;
interface BuildStatusResponseArgs<T> {
monitor: Monitor;
response?: Response<T> | null;
error?: Error | RequestError | HTTPError | null;
payload?: T | null;
jsonPath?: string;
matchMethod?: MonitorStatusResponse["matchMethod"];
expectedValue?: string;
extracted?: unknown;
overrides?: MonitorStatusResponseOverrides<T>;
}
export const buildStatusResponse = <T>({
monitor,
response,
error,
payload,
jsonPath,
matchMethod,
expectedValue,
extracted,
overrides,
}: BuildStatusResponseArgs<T>): MonitorStatusResponse<T> => {
if (error) {
const statusResponse: MonitorStatusResponse<T> = {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: NETWORK_ERROR,
message: error.message ?? "Network error",
responseTime: 0,
timings: undefined,
jsonPath,
matchMethod,
expectedValue,
extracted,
payload,
};
if (error instanceof HTTPError || error instanceof RequestError) {
statusResponse.code = error?.response?.statusCode ?? NETWORK_ERROR;
statusResponse.message = error.message;
statusResponse.responseTime = error.timings?.phases?.total ?? 0;
statusResponse.timings = error.timings;
}
return { ...statusResponse, ...(overrides ?? {}) };
}
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: response?.ok ?? false,
code: response?.statusCode ?? NETWORK_ERROR,
message: response?.statusMessage ?? "",
responseTime: response?.timings?.phases?.total ?? 0,
timings: response?.timings,
payload: payload ?? response?.body,
jsonPath,
matchMethod,
expectedValue,
extracted,
};
};
@@ -1,38 +1,14 @@
import type {
Monitor,
MonitorStatusResponse,
GrpcStatusPayload,
PageSpeedStatusPayload,
HttpStatusPayload,
HardwareStatusPayload,
PingStatusPayload,
DockerStatusPayload,
PortStatusPayload,
GameStatusPayload,
} from "@/types/index.js";
import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
import type { AxiosStatic } from "axios";
import { AppError } from "@/utils/AppError.js";
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
import { ILogger } from "@/utils/logger.js";
import { IStatusProvider } from "./network/IStatusProvider.js";
const SERVICE_NAME = "NetworkService";
export interface INetworkService {
readonly serviceName: string;
requestStatus(
monitor: Monitor
): Promise<
MonitorStatusResponse<
| PingStatusPayload
| HttpStatusPayload
| PageSpeedStatusPayload
| HardwareStatusPayload
| DockerStatusPayload
| PortStatusPayload
| GameStatusPayload
| GrpcStatusPayload
>
>;
requestStatus(monitor: Monitor): Promise<MonitorStatusResponse<unknown>>;
requestWebhook(
type: string,
url: string,
@@ -47,66 +23,19 @@ export interface INetworkService {
}>;
}
class NetworkService implements INetworkService {
export class NetworkService implements INetworkService {
static SERVICE_NAME = SERVICE_NAME;
private TYPE_PING: string;
private TYPE_HTTP: string;
private TYPE_PAGESPEED: string;
private TYPE_HARDWARE: string;
private TYPE_DOCKER: string;
private TYPE_PORT: string;
private TYPE_GAME: string;
private TYPE_GRPC: string;
private SERVICE_NAME: string;
private NETWORK_ERROR: number;
private PING_ERROR: number;
private axios: AxiosStatic;
private logger: ILogger;
private pingProvider;
private httpProvider;
private pageSpeedProvider;
private hardwareProvider;
private dockerProvider;
private portProvider;
private gameProvider;
private grpcProvider;
constructor(
axios: AxiosStatic,
logger: ILogger,
pingProvider: IStatusProvider<PingStatusPayload>,
httpProvider: IStatusProvider<HttpStatusPayload>,
pagespeedProvider: IStatusProvider<PageSpeedStatusPayload>,
hardwareProvider: IStatusProvider<HardwareStatusPayload>,
dockerProvider: IStatusProvider<DockerStatusPayload>,
portProvider: IStatusProvider<PortStatusPayload>,
gameProvider: IStatusProvider<GameStatusPayload>,
grpcProvider: IStatusProvider<GrpcStatusPayload>
private providers: IStatusProvider<unknown>[]
) {
this.TYPE_PING = "ping";
this.TYPE_HTTP = "http";
this.TYPE_PAGESPEED = "pagespeed";
this.TYPE_HARDWARE = "hardware";
this.TYPE_DOCKER = "docker";
this.TYPE_PORT = "port";
this.TYPE_GAME = "game";
this.TYPE_GRPC = "grpc";
this.SERVICE_NAME = SERVICE_NAME;
this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001;
this.axios = axios;
this.logger = logger;
this.pingProvider = pingProvider;
this.httpProvider = httpProvider;
this.pageSpeedProvider = pagespeedProvider;
this.hardwareProvider = hardwareProvider;
this.dockerProvider = dockerProvider;
this.portProvider = portProvider;
this.gameProvider = gameProvider;
this.grpcProvider = grpcProvider;
}
get serviceName(): string {
@@ -114,41 +43,12 @@ class NetworkService implements INetworkService {
}
// Main entry point
async requestStatus(
monitor: Monitor
): Promise<
MonitorStatusResponse<
| PingStatusPayload
| HttpStatusPayload
| PageSpeedStatusPayload
| HardwareStatusPayload
| DockerStatusPayload
| PortStatusPayload
| GameStatusPayload
| GrpcStatusPayload
>
> {
const type = monitor?.type || "unknown";
switch (type) {
case this.TYPE_PING:
return await this.pingProvider.handle(monitor);
case this.TYPE_HTTP:
return await this.httpProvider.handle(monitor);
case this.TYPE_PAGESPEED:
return await this.pageSpeedProvider.handle(monitor);
case this.TYPE_HARDWARE:
return await this.hardwareProvider.handle(monitor);
case this.TYPE_DOCKER:
return await this.dockerProvider.handle(monitor);
case this.TYPE_PORT:
return await this.portProvider.handle(monitor);
case this.TYPE_GAME:
return await this.gameProvider.handle(monitor);
case this.TYPE_GRPC:
return await this.grpcProvider.handle(monitor);
default:
return this.handleUnsupportedType(type);
async requestStatus(monitor: Monitor) {
const provider = this.providers.find((p) => p.supports(monitor.type));
if (!provider) {
return this.handleUnsupportedType(monitor.type);
}
return provider.handle(monitor);
}
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
@@ -157,7 +57,7 @@ class NetworkService implements INetworkService {
teamId: "unknown",
type: "unknown",
status: false,
code: this.NETWORK_ERROR,
code: NETWORK_ERROR,
message: `Unsupported type: ${type}`,
};
}
@@ -181,7 +81,7 @@ class NetworkService implements INetworkService {
} catch (err: unknown) {
this.logger.warn({
message: err instanceof Error ? err.message : String(err),
service: this.SERVICE_NAME,
service: SERVICE_NAME,
method: "requestWebhook",
});
@@ -190,7 +90,7 @@ class NetworkService implements INetworkService {
return {
type: "webhook",
status: false,
code: axiosError.response?.status ?? this.NETWORK_ERROR,
code: axiosError.response?.status ?? NETWORK_ERROR,
message: `Failed to send ${type} notification`,
payload: axiosError.response?.data,
};
@@ -199,7 +99,7 @@ class NetworkService implements INetworkService {
return {
type: "webhook",
status: false,
code: this.NETWORK_ERROR,
code: NETWORK_ERROR,
message: `Failed to send ${type} notification`,
};
}
@@ -225,7 +125,7 @@ class NetworkService implements INetworkService {
throw new AppError({
message: originalMessage || "Error sending PagerDuty notification",
service: this.SERVICE_NAME,
service: SERVICE_NAME,
method: "requestPagerDuty",
details: {
responseData: err && typeof err === "object" && "response" in err ? (err as { response?: { data?: unknown } }).response?.data : undefined,
@@ -268,7 +168,7 @@ class NetworkService implements INetworkService {
if (err instanceof Error) {
this.logger.warn({
message: err.message,
service: this.SERVICE_NAME,
service: SERVICE_NAME,
method: "requestMatrix",
});
@@ -276,7 +176,7 @@ class NetworkService implements INetworkService {
const axiosError = err as { response?: { status?: number; data?: unknown } };
return {
status: false,
code: axiosError.response?.status || this.NETWORK_ERROR,
code: axiosError.response?.status || NETWORK_ERROR,
message: "Failed to send Matrix notification",
payload: axiosError.response?.data,
};
@@ -285,17 +185,15 @@ class NetworkService implements INetworkService {
this.logger.warn({
message: String(err),
service: this.SERVICE_NAME,
service: SERVICE_NAME,
method: "requestMatrix",
});
return {
status: false,
code: this.NETWORK_ERROR,
code: NETWORK_ERROR,
message: "Failed to send Matrix notification",
};
}
}
}
export default NetworkService;
+1 -3
View File
@@ -21,7 +21,7 @@ export interface ISettingsService {
getDBSettings(): Promise<Settings>;
}
class SettingsService implements ISettingsService {
export class SettingsService implements ISettingsService {
static SERVICE_NAME = SERVICE_NAME;
private settings: EnvConfig;
private settingsRepository: ISettingsRepository;
@@ -74,5 +74,3 @@ class SettingsService implements ISettingsService {
return settings;
};
}
export default SettingsService;
+3
View File
@@ -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];
+1
View File
@@ -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"),