Merge branch 'upstream/develop' into feat/bulk-edit-notifications and resolve conflicts

This commit is contained in:
ashu130698 committed 2026-03-20 23:54:33 +05:30
commit 05814854df
41 files changed
+1104 -417

No files matched your search

@@ -63,7 +63,6 @@ class MonitorController {
const monitorId = validatedParams.monitorId;
const dateRange = validatedQuery.dateRange;
const normalize = validatedQuery.normalize;
const teamId = requireTeamId(req.user?.teamId);
@@ -71,7 +70,6 @@ class MonitorController {
teamId,
monitorId,
dateRange,
normalize,
});
return res.status(200).json({
success: true,
@@ -327,7 +325,6 @@ class MonitorController {
try {
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query);
const explain = validatedQuery.explain;
const limit = validatedQuery.limit;
const page = validatedQuery.page;
const rowsPerPage = validatedQuery.rowsPerPage;
@@ -346,7 +343,6 @@ class MonitorController {
filter,
field,
order,
explain,
});
return res.status(200).json({
+9 -5
View File
@@ -2,14 +2,15 @@ import { Request, Response, NextFunction } from "express";
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
import { AppError } from "@/utils/AppError.js";
import { IEmailService, ISettingsService } from "@/service/index.js";
const SERVICE_NAME = "SettingsController";
class SettingsController {
static SERVICE_NAME = SERVICE_NAME;
private settingsService: any;
private emailService: any;
constructor(settingsService: any, emailService: any) {
private settingsService: ISettingsService;
private emailService: IEmailService;
constructor(settingsService: ISettingsService, emailService: IEmailService) {
this.settingsService = settingsService;
this.emailService = emailService;
}
@@ -57,9 +58,9 @@ class SettingsController {
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
try {
updateAppSettingsBodyValidation.parse(req.body);
const validatedBody = updateAppSettingsBodyValidation.parse(req.body);
const updatedSettings = await this.settingsService.updateDbSettings(req.body);
const updatedSettings = await this.settingsService.updateDbSettings(validatedBody);
const returnSettings = this.buildAppSettings(updatedSettings);
return res.status(200).json({
success: true,
@@ -95,6 +96,9 @@ class SettingsController {
const context = { testName: "Monitoring System" };
const html = await this.emailService.buildEmail("testEmailTemplate", context);
if (!html) {
throw new AppError({ message: "Failed to build email template.", status: 500 });
}
const messageId = await this.emailService.sendEmail(to, subject, html, {
systemEmailHost,
systemEmailPort,
@@ -0,0 +1,45 @@
import mongoose from "mongoose";
import { logger } from "@/utils/logger.js";
import StatusPageModel from "../models/StatusPage.js";
export async function migrateStatusPageTypeToArray(): Promise<void> {
const SERVICE_NAME = "Migration:MigrateStatusPageTypeToArray";
try {
logger.info({ service: SERVICE_NAME, message: "Starting migration of StatusPage type field from string to array" });
const db = mongoose.connection.db;
if (!db) {
throw new Error("Database connection is not initialized");
}
const result = await StatusPageModel.updateMany(
{
$expr: {
$eq: [{ $type: "$type" }, "string"], // only scalar string, not existing array
},
},
[
{
$set: {
type: ["$type"],
},
},
]
);
if (result.modifiedCount === 0) {
logger.info({ service: SERVICE_NAME, message: "No StatusPage documents needed migration" });
return;
}
logger.info({
service: SERVICE_NAME,
message: `Migration complete. Converted ${result.modifiedCount} StatusPage document(s) from string type to array type`,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error({ service: SERVICE_NAME, message: `Error during StatusPage type migration: ${errorMessage}` });
throw error;
}
}
+2
View File
@@ -3,6 +3,7 @@ import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
import MigrationModel from "../models/Migration.js";
import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js";
type MigrationEntry = {
name: string;
@@ -14,6 +15,7 @@ const migrations: MigrationEntry[] = [
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
{ name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray },
];
const runMigrations = async (logger?: { info: Function; error: Function }) => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { Schema, model, type Types } from "mongoose";
import type { MaintenanceWindow, DurationUnit } from "@/types/maintenanceWindow.js";
import type { MaintenanceWindow } from "@/types/maintenanceWindow.js";
type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt"> & {
monitorId: Types.ObjectId;
+168 -2
View File
@@ -1,6 +1,18 @@
import { Schema, model, Types, type UpdateQuery } from "mongoose";
import { Schema, model, Types } from "mongoose";
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
import type {
CheckAudits,
CheckCaptureInfo,
CheckCpuInfo,
CheckDiskInfo,
CheckErrorInfo,
CheckHostInfo,
CheckMemoryInfo,
CheckNetworkInterfaceInfo,
GotTimings,
ILighthouseAudit,
} from "@/types/check.js";
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
@@ -23,13 +35,167 @@ interface MonitorDocument extends MonitorDocumentBase {
updatedAt: Date;
}
const snapshotTimingPhasesSchema = new Schema<GotTimings["phases"]>(
{
wait: { type: Number },
dns: { type: Number },
tcp: { type: Number },
tls: { type: Number },
request: { type: Number },
firstByte: { type: Number },
download: { type: Number },
total: { type: Number },
},
{ _id: false }
);
const snapshotTimingsSchema = new Schema<GotTimings>(
{
start: { type: Number },
socket: { type: Number },
lookup: { type: Number },
connect: { type: Number },
secureConnect: { type: Number },
upload: { type: Number },
response: { type: Number },
end: { type: Number },
phases: { type: snapshotTimingPhasesSchema },
},
{ _id: false }
);
const snapshotCpuSchema = new Schema<CheckCpuInfo>(
{
physical_core: { type: Number },
logical_core: { type: Number },
frequency: { type: Number },
current_frequency: { type: Number },
temperature: { type: [Number] },
free_percent: { type: Number },
usage_percent: { type: Number },
},
{ _id: false }
);
const snapshotMemorySchema = new Schema<CheckMemoryInfo>(
{
total_bytes: { type: Number },
available_bytes: { type: Number },
used_bytes: { type: Number },
usage_percent: { type: Number },
},
{ _id: false }
);
const snapshotDiskSchema = new Schema<CheckDiskInfo>(
{
device: { type: String },
mountpoint: { type: String },
total_bytes: { type: Number },
free_bytes: { type: Number },
used_bytes: { type: Number },
usage_percent: { type: Number },
total_inodes: { type: Number },
free_inodes: { type: Number },
used_inodes: { type: Number },
inodes_usage_percent: { type: Number },
read_bytes: { type: Number },
write_bytes: { type: Number },
read_time: { type: Number },
write_time: { type: Number },
},
{ _id: false }
);
const snapshotHostSchema = new Schema<CheckHostInfo>(
{
os: { type: String },
platform: { type: String },
kernel_version: { type: String },
pretty_name: { type: String },
},
{ _id: false }
);
const snapshotErrorSchema = new Schema<CheckErrorInfo>(
{
metric: { type: [String] },
err: { type: String },
},
{ _id: false }
);
const snapshotCaptureSchema = new Schema<CheckCaptureInfo>(
{
version: { type: String },
mode: { type: String },
},
{ _id: false }
);
const snapshotNetworkInterfaceSchema = new Schema<CheckNetworkInterfaceInfo>(
{
name: { type: String },
bytes_sent: { type: Number },
bytes_recv: { type: Number },
packets_sent: { type: Number },
packets_recv: { type: Number },
err_in: { type: Number },
err_out: { type: Number },
drop_in: { type: Number },
drop_out: { type: Number },
fifo_in: { type: Number },
fifo_out: { type: Number },
},
{ _id: false }
);
const snapshotLighthouseAuditSchema = new Schema<ILighthouseAudit>(
{
id: { type: String },
title: { type: String },
score: { type: Number },
displayValue: { type: String },
numericValue: { type: Number },
numericUnit: { type: String },
},
{ _id: false }
);
const snapshotAuditsSchema = new Schema<CheckAudits>(
{
cls: { type: snapshotLighthouseAuditSchema },
si: { type: snapshotLighthouseAuditSchema },
fcp: { type: snapshotLighthouseAuditSchema },
lcp: { type: snapshotLighthouseAuditSchema },
tbt: { type: snapshotLighthouseAuditSchema },
},
{ _id: false }
);
const checkSnapshotSchema = new Schema<CheckSnapshotDocument>(
{
id: { type: String, required: true },
status: { type: Boolean, required: true },
responseTime: { type: Number },
timings: { type: snapshotTimingsSchema },
statusCode: { type: Number },
message: { type: String },
cpu: { type: snapshotCpuSchema },
memory: { type: snapshotMemorySchema },
disk: { type: [snapshotDiskSchema] },
host: { type: snapshotHostSchema },
errors: { type: [snapshotErrorSchema] },
capture: { type: snapshotCaptureSchema },
net: { type: [snapshotNetworkInterfaceSchema] },
accessibility: { type: Number },
bestPractices: { type: Number },
seo: { type: Number },
performance: { type: Number },
audits: { type: snapshotAuditsSchema },
createdAt: { type: Date, required: true },
},
{ _id: false, strict: false }
{ _id: false }
);
const MonitorSchema = new Schema<MonitorDocument>(
+6 -2
View File
@@ -43,9 +43,9 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
required: true,
},
type: {
type: String,
type: [String],
required: true,
default: "uptime",
default: ["uptime"],
enum: StatusPageTypes,
},
companyName: {
@@ -100,6 +100,10 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
type: Boolean,
default: false,
},
showInfrastructure: {
type: Boolean,
default: false,
},
customCSS: {
type: String,
default: "",
+9 -6
View File
@@ -1,15 +1,18 @@
import type { NextFunction, Request, Response } from "express";
import { logger } from "@/utils/logger.js";
import { AppError } from "@/utils/AppError.js";
const handleErrors = (error: any, req: Request, res: Response, next: NextFunction) => {
const status = error.status || 500;
const message = error.message || "Server error";
const service = error.service || "unknownService";
const handleErrors = (error: unknown, req: Request, res: Response, _next: NextFunction) => {
const status = error instanceof AppError ? error.status || 500 : 500;
const message = error instanceof AppError ? error.message : "Server error";
const service = error instanceof AppError ? error.service : "unknownService";
const method = error instanceof AppError ? error.method : "unknownMethod";
logger.error({
message: message,
service: service,
method: error.method,
stack: error.stack,
method: method,
stack: error instanceof AppError ? error.stack : undefined,
details: error instanceof AppError ? error.details : undefined,
});
res.status(status).json({
status,
+12 -5
View File
@@ -1,7 +1,9 @@
import type { Request, Response, NextFunction } from "express";
import DOMPurify from "isomorphic-dompurify";
export const sanitizeInput = (input: any, options = {}) => {
export function sanitizeInput(input: string, options?: object): string;
export function sanitizeInput(input: unknown, options?: object): unknown;
export function sanitizeInput(input: unknown, options = {}): unknown {
if (typeof input !== "string") {
return input;
}
@@ -15,9 +17,9 @@ export const sanitizeInput = (input: any, options = {}) => {
};
return DOMPurify.sanitize(input, defaultConfig);
};
}
export const sanitizeObject = (obj: Record<string, any>, options = {}): Record<string, any> => {
export const sanitizeObject = (obj: unknown, options = {}): unknown => {
if (typeof obj !== "object" || obj === null) {
return obj;
}
@@ -26,7 +28,7 @@ export const sanitizeObject = (obj: Record<string, any>, options = {}): Record<s
return obj.map((item) => sanitizeObject(item, options));
}
const sanitized: Record<string, any> = {};
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === "string") {
sanitized[key] = sanitizeInput(value, options);
@@ -52,7 +54,12 @@ export const sanitizeBody = (options = {}): ((req: Request, res: Response, next:
export const sanitizeQuery = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => {
return (req: Request, res: Response, next: NextFunction) => {
if (req.query && typeof req.query === "object") {
req.query = sanitizeObject(req.query, options);
for (const key of Object.keys(req.query)) {
const value = req.query[key];
if (typeof value === "string") {
req.query[key] = sanitizeInput(value, options);
}
}
}
next();
};
+11 -5
View File
@@ -3,10 +3,15 @@ import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken";
import { AppError } from "@/utils/AppError.js";
import { Settings } from "@/types/settings.js";
import type { User } from "@/types/user.js";
const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer ";
const isUser = (payload: unknown): payload is User => {
return typeof payload === "object" && payload !== null && "id" in payload && "teamId" in payload && "role" in payload;
};
export const createVerifyJWT = (settingsService: { getSettings: () => Settings }) => {
return (req: Request, res: Response, next: NextFunction) => {
const token = req.headers["authorization"];
@@ -31,21 +36,22 @@ export const createVerifyJWT = (settingsService: { getSettings: () => Settings }
next(error);
return;
}
jwt.verify(parsedToken, jwtSecret, (err: any, decoded: any) => {
jwt.verify(parsedToken, jwtSecret, (err: jwt.VerifyErrors | null, decoded: jwt.JwtPayload | string | undefined) => {
if (err) {
const error = new AppError({
message: "Failed to authenticate token",
details: err,
message: err instanceof Error ? err.message : "Failed to authenticate token",
status: 401,
service: SERVICE_NAME,
details: err instanceof Error ? { error: err } : undefined,
method: "verifyJWT",
});
next(error);
return;
} else {
// Token is valid, carry on
} else if (isUser(decoded)) {
req.user = decoded;
next();
} else {
next(new AppError({ message: "Invalid token payload", status: 401, service: SERVICE_NAME, method: "verifyJWT" }));
}
});
};
@@ -1,11 +1,11 @@
import type { Settings } from "@/types/index.js";
import type { Settings, SettingsUpdate } from "@/types/index.js";
export interface ISettingsRepository {
// create
create(settings: Partial<Settings>): Promise<Settings>;
// fetch
findSingleton(): Promise<Settings | null>;
// update
update(settings: Partial<Settings>): Promise<Settings>;
update(settings: SettingsUpdate): Promise<Settings>;
// delete
deleteLegacy: () => Promise<boolean>;
}
@@ -1,6 +1,6 @@
import mongoose from "mongoose";
import mongoose, { type UpdateQuery } from "mongoose";
import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js";
import type { Settings } from "@/types/index.js";
import type { Settings, SettingsUpdate } from "@/types/index.js";
import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js";
class MongoSettingsRepository implements ISettingsRepository {
@@ -59,27 +59,23 @@ class MongoSettingsRepository implements ISettingsRepository {
return this.toEntity(settings);
};
update = async (settings: Partial<Settings>) => {
const update: Record<string, any> = { $set: {}, $unset: {} };
update = async (settings: SettingsUpdate) => {
const $set: Record<string, unknown> = {};
const $unset: Record<string, string> = {};
// Iterate through settings and separate into $set and $unset
Object.entries(settings).forEach(([key, value]) => {
if (value === undefined || value === "") {
// Unset undefined or empty string values
update.$unset[key] = "";
if (value === undefined || value === null) {
$unset[key] = "";
} else {
// Set defined values
update.$set[key] = value;
$set[key] = value;
}
});
// Clean up empty operators
if (Object.keys(update.$set).length === 0) {
delete update.$set;
}
if (Object.keys(update.$unset).length === 0) {
delete update.$unset;
}
const update: UpdateQuery<AppSettingsDocument> = {
...(Object.keys($set).length > 0 && { $set }),
...(Object.keys($unset).length > 0 && { $unset }),
};
await AppSettingsModel.findOneAndUpdate({}, update, {
upsert: true,
@@ -57,6 +57,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
showCharts: doc.showCharts,
showUptimePercentage: doc.showUptimePercentage,
showAdminLoginLink: doc.showAdminLoginLink,
showInfrastructure: doc.showInfrastructure,
customCSS: doc.customCSS,
createdAt: this.toDateString(doc.createdAt),
updatedAt: this.toDateString(doc.updatedAt),
+51 -45
View File
@@ -7,6 +7,7 @@ import type {
HardwareDetailsResult,
PageSpeedDetailsResult,
GamesMap,
GroupedGeoCheckResult,
} from "@/types/monitor.js";
import { supportsGeoCheck } from "@/types/monitor.js";
import type { GeoContinent } from "@/types/geoCheck.js";
@@ -20,7 +21,10 @@ import type {
} from "@/repositories/index.js";
import demoMonitorsData from "@/utils/demoMonitors.json" with { type: "json" };
import { AppError } from "@/utils/AppError.js";
import type { ImportedMonitor } from "@/validation/monitorValidation.js";
import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import { IEmailService } from "../infrastructure/emailService.js";
import { ILogger } from "@/utils/logger.js";
const SERVICE_NAME = "MonitorService";
type DateRangeKey = "recent" | "day" | "week" | "month" | "all";
@@ -30,14 +34,19 @@ export interface IMonitorService {
// create
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>;
createMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
createMonitors(monitors: Array<Monitor>): Promise<Monitor[] | null>;
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
// read
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<UptimeDetailsResult>;
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<UptimeDetailsResult>;
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<HardwareDetailsResult>;
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<PageSpeedDetailsResult>;
getGeoChecksByMonitorId(args: { teamId: string; monitorId: string; dateRange: string; continents?: GeoContinent[] }): Promise<any>;
getGeoChecksByMonitorId(args: {
teamId: string;
monitorId: string;
dateRange: string;
continents?: GeoContinent[];
}): Promise<GroupedGeoCheckResult>;
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
getMonitorsByTeamId(args: {
teamId: string;
@@ -58,7 +67,6 @@ export interface IMonitorService {
filter?: string;
field?: string;
order?: "asc" | "desc";
explain?: boolean;
}): Promise<MonitorsWithChecksByTeamIdResult>;
getAllGames(): GamesMap;
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
@@ -76,16 +84,16 @@ export interface IMonitorService {
// other
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): Promise<{ imported: number; errors: string[] }>;
importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: ImportedMonitor[] }): Promise<{ imported: number; errors: string[] }>;
}
export class MonitorService implements IMonitorService {
static SERVICE_NAME = SERVICE_NAME;
private jobQueue: ISuperSimpleQueue;
private emailService: any;
private logger: any;
private games: any;
private emailService: IEmailService;
private logger: ILogger;
private games: GamesMap;
private monitorsRepository: IMonitorsRepository;
private checksRepository: IChecksRepository;
private geoChecksRepository: IGeoChecksRepository;
@@ -106,9 +114,9 @@ export class MonitorService implements IMonitorService {
incidentsRepository,
}: {
jobQueue: ISuperSimpleQueue;
emailService: any;
logger: any;
games: any;
emailService: IEmailService;
logger: ILogger;
games: GamesMap;
monitorsRepository: IMonitorsRepository;
checksRepository: IChecksRepository;
geoChecksRepository: IGeoChecksRepository;
@@ -166,7 +174,7 @@ export class MonitorService implements IMonitorService {
this.jobQueue.addJob(monitor.id, monitor);
};
createMonitors = async (monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null> => {
createMonitors = async (monitors: Array<Monitor>): Promise<Monitor[] | null> => {
const createdMonitors = await this.monitorsRepository.createMonitors(monitors);
if (!monitors || monitors.length === 0) {
throw new AppError({ message: "Failed to create monitors", status: 500, service: SERVICE_NAME, method: "createMonitors" });
@@ -176,7 +184,7 @@ export class MonitorService implements IMonitorService {
return createdMonitors;
};
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<any[]> => {
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<Monitor[]> => {
const demoData = demoMonitorsData;
const monitors = demoData.map((monitor) => ({
userId,
@@ -197,12 +205,10 @@ export class MonitorService implements IMonitorService {
teamId,
monitorId,
dateRange,
normalize,
}: {
teamId: string;
monitorId: string;
dateRange: string;
normalize?: boolean;
}): Promise<UptimeDetailsResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
@@ -330,7 +336,7 @@ export class MonitorService implements IMonitorService {
monitorId: string;
dateRange: string;
continents?: GeoContinent[];
}): Promise<any> => {
}): Promise<GroupedGeoCheckResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
@@ -383,7 +389,6 @@ export class MonitorService implements IMonitorService {
filter,
field,
order,
explain,
}: {
teamId: string;
limit?: number;
@@ -393,7 +398,6 @@ export class MonitorService implements IMonitorService {
filter?: string;
field?: string;
order?: "asc" | "desc";
explain?: boolean;
}): Promise<MonitorsWithChecksByTeamIdResult> => {
const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId, { type });
const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter });
@@ -462,47 +466,51 @@ export class MonitorService implements IMonitorService {
pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId);
monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor);
if (monitor.isActive) {
await this.jobQueue.resumeJob(monitor);
} else {
await this.jobQueue.pauseJob(monitor);
}
return monitor;
};
deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
const monitor = await this.monitorsRepository.deleteById(monitorId, teamId);
await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => {
this.logger.warn({
message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`,
service: SERVICE_NAME,
stack: err.stack,
stack: err instanceof Error ? err.stack : undefined,
});
});
await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => {
this.logger.warn({
message: `Error deleting checks for monitor ${monitor.id} with name ${monitor.name}`,
service: SERVICE_NAME,
stack: err.stack,
stack: err instanceof Error ? err.stack : undefined,
});
});
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: any) => {
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: unknown) => {
this.logger.warn({
message: `Error removing monitor ${monitor.id} with name ${monitor.name} from status pages`,
service: SERVICE_NAME,
stack: err.stack,
stack: err instanceof Error ? err.stack : undefined,
});
});
await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId).catch((err: any) => {
await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId).catch((err: unknown) => {
this.logger.warn({
message: `Error deleting incidents for monitor ${monitor.id} with name ${monitor.name}`,
service: SERVICE_NAME,
stack: err.stack,
stack: err instanceof Error ? err.stack : undefined,
});
});
await this.geoChecksRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
await this.geoChecksRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => {
this.logger.warn({
message: `Error deleting geo checks for monitor ${monitor.id} with name ${monitor.name}`,
service: SERVICE_NAME,
stack: err.stack,
stack: err instanceof Error ? err.stack : undefined,
});
});
@@ -520,12 +528,12 @@ export class MonitorService implements IMonitorService {
await this.geoChecksRepository.deleteByMonitorId(monitor.id);
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id);
await this.monitorStatsRepository.deleteByMonitorId(monitor.id);
} catch (error: any) {
} catch (error: unknown) {
this.logger.warn({
message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`,
service: SERVICE_NAME,
method: "deleteAllMonitors",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
}
})
@@ -533,7 +541,7 @@ export class MonitorService implements IMonitorService {
return deletedCount;
};
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<any[]> => {
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<Monitor[]> => {
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
if (!monitors || monitors.length === 0) {
@@ -550,23 +558,21 @@ export class MonitorService implements IMonitorService {
}: {
teamId: string;
userId: string;
monitors: any[];
monitors: ImportedMonitor[];
}): Promise<{ imported: number; errors: string[] }> => {
const errors: string[] = [];
const cleanedMonitors = monitors.map((monitor) => {
const cleanData = { ...monitor };
delete cleanData.id;
delete cleanData._id;
delete cleanData.createdAt;
delete cleanData.updatedAt;
delete cleanData.recentChecks;
// Monitors must belong to current team
cleanData.teamId = teamId;
return cleanData;
});
const cleanedMonitors: Monitor[] = monitors.map((monitor) => ({
...monitor,
id: "",
teamId,
userId,
recentChecks: [],
createdAt: "",
updatedAt: "",
}));
const createdMonitors = await this.createMonitors(cleanedMonitors, userId, teamId);
const createdMonitors = await this.createMonitors(cleanedMonitors);
if (!createdMonitors || createdMonitors.length === 0) {
throw new AppError({
@@ -360,8 +360,7 @@ export class StatusService implements IStatusService {
};
} catch (error: unknown) {
throw new AppError({
message: `Failed to update monitor status: ${error instanceof Error ? error.message : "Unknown error"}`,
details: `Error updating status for monitor with id ${check.metadata.monitorId}`,
message: `Failed to update monitor with id ${check.metadata.monitorId} with status: ${error instanceof Error ? error.message : "Unknown error"}`,
service: SERVICE_NAME,
method: "updateMonitorStatus",
});
+3 -2
View File
@@ -1,5 +1,5 @@
import { ISettingsRepository } from "@/repositories/index.js";
import { Settings } from "@/types/index.js";
import { Settings, SettingsUpdate } from "@/types/index.js";
import { AppError } from "@/utils/AppError.js";
import { ValidatedEnv } from "@/validation/envValidation.js";
import type { StringValue } from "ms";
@@ -19,6 +19,7 @@ export interface ISettingsService {
loadSettings(): EnvConfig;
getSettings(): EnvConfig;
getDBSettings(): Promise<Settings>;
updateDbSettings(newSettings: SettingsUpdate): Promise<Settings>;
}
export class SettingsService implements ISettingsService {
@@ -53,7 +54,7 @@ export class SettingsService implements ISettingsService {
return this.settings;
}
updateDbSettings = async (newSettings: Partial<Settings>) => {
updateDbSettings = async (newSettings: SettingsUpdate) => {
return await this.settingsRepository.update(newSettings);
};
+5 -1
View File
@@ -1,6 +1,6 @@
import type { CheckSnapshot } from "@/types/check.js";
export type { CheckSnapshot } from "@/types/check.js";
import type { GeoContinent } from "@/types/geoCheck.js";
import type { GeoContinent, GroupedGeoCheck } from "@/types/geoCheck.js";
export type { GeoContinent } from "@/types/geoCheck.js";
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const;
@@ -74,6 +74,10 @@ export interface MonitorsWithChecksByTeamIdResult {
monitors: Monitor[];
}
export interface GroupedGeoCheckResult {
groupedGeoChecks: GroupedGeoCheck[];
}
export interface UptimeDetailsResult {
monitorData: {
monitor: Monitor;
+4
View File
@@ -5,6 +5,10 @@ export interface SettingsThresholds {
temperature?: number;
}
export type SettingsUpdate = {
[K in keyof Settings]?: Settings[K] | null;
};
export interface Settings {
id: string;
checkTTL: number;
+3 -2
View File
@@ -1,4 +1,4 @@
export const StatusPageTypes = ["uptime"] as const;
export const StatusPageTypes = ["uptime", "infrastructure"] as const;
export type StatusPageType = (typeof StatusPageTypes)[number];
export interface StatusPageLogo {
@@ -15,7 +15,7 @@ export interface StatusPage {
id: string;
userId: string;
teamId: string;
type: StatusPageType;
type: StatusPageType[];
companyName: string;
url: string;
timezone?: string;
@@ -28,6 +28,7 @@ export interface StatusPage {
showCharts: boolean;
showUptimePercentage: boolean;
showAdminLoginLink: boolean;
showInfrastructure: boolean;
customCSS: string;
createdAt: string;
updatedAt: string;
+9 -9
View File
@@ -1,18 +1,18 @@
export interface AppErrorConfig {
message: string;
message?: string;
status?: number;
service?: string | null;
method?: string | null;
details?: unknown;
service?: string;
method?: string;
details?: Record<string, unknown> | undefined;
}
export class AppError extends Error {
private status: number;
private service: string | null;
private method: string | null;
private details: unknown;
readonly status: number;
readonly service: string;
readonly method: string;
readonly details: Record<string, unknown> | undefined;
constructor({ message, status = 500, service = null, method = null, details = null }: AppErrorConfig) {
constructor({ message, status = 500, service = "unknownService", method = "unknownMethod", details = undefined }: AppErrorConfig) {
super(message);
this.status = status;
this.service = service;
+3 -2
View File
@@ -45,7 +45,7 @@ export const NormalizeData = <T extends HasResponseTime>(checks: T[], rangeMin:
const min = calculatePercentile(checks, 0);
const max = calculatePercentile(checks, 95);
const normalizedChecks = checks.map((check) => {
const originalResponseTime = check.responseTime;
const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0;
let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min);
normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime));
@@ -59,7 +59,8 @@ export const NormalizeData = <T extends HasResponseTime>(checks: T[], rangeMin:
return normalizedChecks;
} else {
return checks.map((check) => {
return { ...check, originalResponseTime: check.responseTime };
const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0;
return { ...check, originalResponseTime: originalResponseTime };
});
}
};
+25 -24
View File
@@ -128,39 +128,38 @@ const importedMonitorSchema = z.object({
teamId: z.string().optional(),
name: z.string().min(1, "Name is required"),
description: z.union([z.string(), z.literal("")]).optional(),
status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).optional(),
statusWindow: z.array(z.boolean()).optional(),
statusWindowSize: z.number().optional(),
statusWindowThreshold: z.number().optional(),
status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).default("initializing"),
statusWindow: z.array(z.boolean()).default([]),
statusWindowSize: z.number().min(1).max(20).default(5),
statusWindowThreshold: z.number().min(1).max(100).default(60),
type: z.enum(MonitorTypes, "Invalid monitor type"),
ignoreTlsErrors: z.boolean().optional(),
useAdvancedMatching: z.boolean().optional(),
ignoreTlsErrors: z.boolean().default(false),
useAdvancedMatching: z.boolean().default(false),
jsonPath: z.union([z.string(), z.literal("")]).optional(),
expectedValue: z.union([z.string(), z.literal("")]).optional(),
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
url: z.string().min(1, "URL is required"),
port: z.number().optional(),
isActive: z.boolean().optional(),
interval: z.number().optional(),
isActive: z.boolean().default(true),
interval: z.number().default(60000),
uptimePercentage: z.number().optional(),
notifications: z.array(z.string()).optional(),
notifications: z.array(z.string()).default([]),
secret: z.string().optional(),
cpuAlertThreshold: z.number().optional(),
cpuAlertCounter: z.number().optional(),
memoryAlertThreshold: z.number().optional(),
memoryAlertCounter: z.number().optional(),
diskAlertThreshold: z.number().optional(),
diskAlertCounter: z.number().optional(),
tempAlertThreshold: z.number().optional(),
tempAlertCounter: z.number().optional(),
selectedDisks: z.array(z.string()).optional(),
cpuAlertThreshold: z.number().default(100),
cpuAlertCounter: z.number().default(5),
memoryAlertThreshold: z.number().default(100),
memoryAlertCounter: z.number().default(5),
diskAlertThreshold: z.number().default(100),
diskAlertCounter: z.number().default(5),
tempAlertThreshold: z.number().default(100),
tempAlertCounter: z.number().default(5),
selectedDisks: z.array(z.string()).default([]),
gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).optional(),
group: z.union([z.string(), z.null()]).optional(),
geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
geoCheckInterval: z.number().optional(),
recentChecks: z.array(z.unknown()).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
group: z.union([z.string().max(50).trim(), z.null()]).default(null),
geoCheckEnabled: z.boolean().default(false),
geoCheckLocations: z.array(z.enum(GeoContinents)).default([]),
geoCheckInterval: z.number().min(300000).default(300000),
createdAt: z.string().optional(),
updatedAt: z.string().optional(),
});
@@ -169,6 +168,8 @@ export const importMonitorsBodyValidation = z.object({
monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"),
});
export type ImportedMonitor = z.output<typeof importedMonitorSchema>;
export const getHardwareDetailsByIdParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
+15 -15
View File
@@ -7,17 +7,17 @@ import { z } from "zod";
export const updateAppSettingsBodyValidation = z
.object({
checkTTL: z.union([z.number().int().min(1).max(CHECK_TTL_SENTINEL), z.literal("")]).optional(),
systemEmailPort: z.union([z.number(), z.literal("")]).optional(),
pagespeedApiKey: z.union([z.string(), z.literal("")]).optional(),
language: z.union([z.string(), z.literal("")]).optional(),
timezone: z.union([z.string(), z.literal("")]).optional(),
systemEmailHost: z.union([z.string(), z.literal("")]).optional(),
systemEmailAddress: z.union([z.string(), z.literal("")]).optional(),
systemEmailPassword: z.union([z.string(), z.literal("")]).optional(),
systemEmailUser: z.union([z.string(), z.literal("")]).optional(),
systemEmailConnectionHost: z.union([z.string(), z.literal("")]).optional(),
systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(),
checkTTL: z.number().int().min(1).max(CHECK_TTL_SENTINEL).optional(),
systemEmailPort: z.number().nullable().optional(),
pagespeedApiKey: z.string().nullable().optional(),
language: z.string().optional(),
timezone: z.string().optional(),
systemEmailHost: z.string().nullable().optional(),
systemEmailAddress: z.string().nullable().optional(),
systemEmailPassword: z.string().nullable().optional(),
systemEmailUser: z.string().nullable().optional(),
systemEmailConnectionHost: z.string().nullable().optional(),
systemEmailTLSServername: z.string().nullable().optional(),
showURL: z.boolean().optional(),
systemEmailSecure: z.boolean().optional(),
@@ -28,10 +28,10 @@ export const updateAppSettingsBodyValidation = z
globalThresholds: z
.object({
cpu: z.union([z.number().min(1).max(100), z.literal("")]).optional(),
memory: z.union([z.number().min(1).max(100), z.literal("")]).optional(),
disk: z.union([z.number().min(1).max(100), z.literal("")]).optional(),
temperature: z.union([z.number().min(1).max(150), z.literal("")]).optional(),
cpu: z.number().min(1).max(100).optional(),
memory: z.number().min(1).max(100).optional(),
disk: z.number().min(1).max(100).optional(),
temperature: z.number().min(1).max(150).optional(),
})
.optional(),
})
@@ -1,5 +1,6 @@
import { z } from "zod";
import { booleanCoercion } from "./shared.js";
import { StatusPageTypes } from "@/types/statusPage.js";
//****************************************
// Status Page Validations
@@ -10,13 +11,13 @@ export const getStatusPageParamValidation = z.object({
});
export const getStatusPageQueryValidation = z.object({
type: z.literal("uptime"),
type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])),
timeFrame: z.coerce.number().optional(),
});
export const createStatusPageBodyValidation = z
.object({
type: z.literal("uptime"),
type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])),
companyName: z.string().min(1, "Company name is required"),
url: z.string().regex(/^[a-zA-Z0-9_-]+$/, {
message: "URL can only contain letters, numbers, underscores, and hyphens",
@@ -30,6 +31,7 @@ export const createStatusPageBodyValidation = z
showCharts: booleanCoercion.optional(),
showUptimePercentage: booleanCoercion,
showAdminLoginLink: booleanCoercion.optional(),
showInfrastructure: booleanCoercion.optional(),
removeLogo: z.union([z.literal("true"), z.literal("false")]).optional(),
})
.strip();