Merge branch 'upstream/develop' into feat/bulk-edit-notifications and resolve conflicts
This commit is contained in:
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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user