reorg to domain and api

This commit is contained in:
Alex Holliday committed 2026-06-09 12:34:15 -07:00
1 parent 0fb210e9e1
commit d022f7d849
190 files changed
+643 -830

No files matched your search

@@ -0,0 +1,51 @@
import { z } from "zod";
import { passwordPattern, nameValidation, lowercaseEmailValidation } from "./shared.js";
import { UserRoles } from "@/domain/users/user.type.js";
//****************************************
// Auth Validations
//****************************************
export const loginValidation = z.object({
email: z.email("Must be a valid email address").transform((val) => val.toLowerCase()),
password: z.string().min(1, "Password is required"),
});
export const registrationBodyValidation = z.object({
firstName: nameValidation,
lastName: nameValidation,
email: lowercaseEmailValidation,
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(passwordPattern, "Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character"),
profileImage: z.any().optional(),
inviteToken: z.string().optional().default(""),
});
export const recoveryValidation = z.object({
email: z.email("Must be a valid email address"),
});
export const recoveryTokenBodyValidation = z.object({
recoveryToken: z.string().min(1, "Recovery token is required"),
});
export const newPasswordValidation = z.object({
recoveryToken: z.string().min(1, "Recovery token is required"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(passwordPattern, "Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character"),
confirm: z.string().optional(),
});
export const inviteBodyValidation = z.object({
email: z.email("Must be a valid email address"),
role: z.array(z.enum(UserRoles)).min(1, "At least one role is required"),
teamId: z.string().min(1, "Team ID is required"),
});
export const inviteVerificationBodyValidation = z.object({
token: z.string().min(1, "Token is required"),
});
@@ -0,0 +1,43 @@
import { z } from "zod";
import { booleanCoercion } from "./shared.js";
import { GeoContinents } from "@/domain/geo-checks/geo-check.type.js";
import { MonitorTypes } from "@/domain/monitors/monitor.types.js";
//****************************************
// Check Validations
//****************************************
export const getChecksParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
export const getChecksQueryValidation = z.object({
type: z.enum(MonitorTypes).optional(),
sortOrder: z.enum(["asc", "desc"]),
limit: z.coerce.number().optional(),
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
filter: z.enum(["all", "up", "down", "resolve"]).optional(),
ack: booleanCoercion.optional(),
page: z.coerce.number(),
rowsPerPage: z.coerce.number(),
status: booleanCoercion.optional(),
continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(),
});
export const getTeamChecksQueryValidation = z.object({
sortOrder: z.enum(["asc", "desc"]),
limit: z.coerce.number().optional(),
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
filter: z.enum(["all", "up", "down", "resolve"]).optional(),
ack: booleanCoercion.optional(),
page: z.coerce.number(),
rowsPerPage: z.coerce.number(),
});
export const deleteChecksParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
export const getChecksSummaryByTeamIdQueryValidation = z.object({
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
});
@@ -0,0 +1,20 @@
import { z } from "zod";
import { booleanCoercion } from "./shared.js";
//****************************************
// Incident Validations
//****************************************
export const getIncidentsByTeamQueryValidation = z.object({
sortOrder: z.enum(["asc", "desc"]),
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).default("all"),
page: z.coerce.number().int().min(0),
rowsPerPage: z.coerce.number().int().min(1),
status: booleanCoercion.optional(),
monitorId: z.string().optional(),
resolutionType: z.enum(["manual", "automatic"]).optional(),
});
export const getIncidentSummaryQueryValidation = z.object({
limit: z.coerce.number().int().min(1).optional(),
});
+24
View File
@@ -0,0 +1,24 @@
/**
* Central validation exports
*
* This file re-exports all validation schemas from their respective modules.
* Import from here for convenience: import { loginValidation } from "@/validation";
*/
// Shared utilities
export * from "./shared.js";
// Environment validation
export * from "@/config/envValidation.js";
// Domain-specific validations
export * from "./authValidation.js";
export * from "./monitorValidation.js";
export * from "./checkValidation.js";
export * from "./maintenanceWindowValidation.js";
export * from "./settingsValidation.js";
export * from "./statusPageValidation.js";
export * from "./notificationValidation.js";
export * from "./userValidation.js";
export * from "./incidentValidation.js";
export * from "./tagValidation.js";
@@ -0,0 +1,92 @@
import { z } from "zod";
import { booleanCoercion } from "./shared.js";
import { DurationUnits } from "@/domain/maintenance-windows/maintenance-window.type.js";
const dateToString = z.coerce.date().transform((d) => d.toISOString());
//****************************************
// Maintenance Window Validations
//****************************************
export const createMaintenanceWindowBodyValidation = z
.object({
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
name: z.string().min(1, "Name is required"),
active: z.boolean().optional(),
duration: z.number().min(1, "Duration is required"),
durationUnit: z.enum(DurationUnits),
start: dateToString,
end: dateToString,
repeat: z.number().min(0, "Repeat must be a non-negative number"),
expiry: dateToString.optional(),
})
.strict()
.superRefine((data, ctx) => {
const start = new Date(data.start).getTime();
const end = new Date(data.end).getTime();
if (end <= start) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "End must be after start",
path: ["end"],
});
}
if (data.repeat === 0 && end <= Date.now()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "End must be in the future for one-time maintenance windows",
path: ["end"],
});
}
});
export const getMaintenanceWindowByIdParamValidation = z.object({
id: z.string().min(1, "ID is required"),
});
export const getMaintenanceWindowsByTeamIdQueryValidation = z.object({
active: booleanCoercion.optional(),
page: z.coerce.number().optional(),
rowsPerPage: z.coerce.number().optional(),
field: z.string().optional(),
order: z.enum(["asc", "desc"]).optional(),
});
export const getMaintenanceWindowsByMonitorIdParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
export const deleteMaintenanceWindowByIdParamValidation = z.object({
id: z.string().min(1, "ID is required"),
});
export const editMaintenanceWindowByIdParamValidation = z.object({
id: z.string().min(1, "ID is required"),
});
export const editMaintenanceByIdWindowBodyValidation = z
.object({
active: z.boolean().optional(),
name: z.string().optional(),
repeat: z.number().optional(),
start: dateToString.optional(),
end: dateToString.optional(),
expiry: dateToString.optional(),
monitors: z.array(z.string()).min(1, "At least one monitor is required").optional(),
duration: z.number().optional(),
durationUnit: z.enum(DurationUnits).optional(),
})
.strict()
.superRefine((data, ctx) => {
if (data.start && data.end) {
const start = new Date(data.start).getTime();
const end = new Date(data.end).getTime();
if (end <= start) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "End must be after start",
path: ["end"],
});
}
}
});
@@ -0,0 +1,276 @@
import { z } from "zod";
import { booleanCoercion, dnsHostnameRegex, dnsServerValidation } from "./shared.js";
import { GeoContinents } from "@/domain/geo-checks/geo-check.type.js";
import {
DnsRecordTypes,
HttpStatusCodeSet,
MonitorMatchMethods,
MonitorStatuses,
MonitorTypes,
PageSpeedStrategies,
} from "@/domain/monitors/monitor.types.js";
const httpStatusCode = z.number().refine((code) => HttpStatusCodeSet.has(code), { message: "Must be a valid HTTP status code" });
export const getMonitorByIdParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
export const getMonitorByIdQueryValidation = z.object({
status: booleanCoercion.optional(),
sortOrder: z.enum(["asc", "desc"]).optional(),
limit: z.coerce.number().optional(),
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
numToDisplay: z.coerce.number().optional(),
normalize: booleanCoercion.optional(),
continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(),
});
export const getMonitorsByTeamIdParamValidation = z.object({});
export const getMonitorsByTeamIdQueryValidation = z.object({
type: z.union([z.enum(MonitorTypes), z.array(z.enum(MonitorTypes))]).optional(),
filter: z.union([z.string(), z.literal("")]).optional(),
tags: z.union([z.string(), z.array(z.string())]).optional(),
});
export const getMonitorsWithChecksQueryValidation = z.object({
limit: z.coerce.number().int().min(1).max(100).optional(),
page: z.coerce.number().int().min(0).optional(),
rowsPerPage: z.coerce.number().int().min(1).max(100).optional(),
filter: z.union([z.string(), z.literal("")]).optional(),
field: z.string().optional(),
order: z.enum(["asc", "desc"]).optional(),
type: z.union([z.enum(MonitorTypes), z.array(z.enum(MonitorTypes))]).optional(),
tags: z.union([z.string(), z.array(z.string())]).optional(),
explain: booleanCoercion.optional(),
});
export const getCertificateParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
const refineDnsHostname = (body: { type?: string; url?: string }, ctx: z.RefinementCtx) => {
if (body.type === "dns" && body.url && !dnsHostnameRegex.test(body.url)) {
ctx.addIssue({
code: "custom",
path: ["url"],
message: "Enter a valid domain (e.g. www.example.com)",
});
}
};
const refineStrategyType = (body: { type?: string; strategy?: string }, ctx: z.RefinementCtx) => {
if (body.strategy !== undefined && body.type !== undefined && body.type !== "pagespeed") {
ctx.addIssue({
code: "custom",
path: ["strategy"],
message: "Strategy is only valid for pagespeed monitors",
});
}
};
export const createMonitorBodyValidation = z
.object({
_id: z.string().optional(),
name: z.string().min(1, "Name is required"),
description: z.union([z.string(), z.literal("")]).optional(),
type: z.enum(MonitorTypes, "Invalid monitor type"),
statusWindowSize: z.number().min(1).max(20).default(5),
statusWindowThreshold: z.number().min(1).max(100).default(60),
url: z.string().min(1, "URL is required"),
ignoreTlsErrors: z.boolean().default(false),
useAdvancedMatching: z.boolean().default(false),
port: z.number().optional(),
isActive: z.boolean().optional(),
interval: z.number().optional(),
cpuAlertThreshold: z.number().optional(),
memoryAlertThreshold: z.number().optional(),
diskAlertThreshold: z.number().optional(),
tempAlertThreshold: z.number().optional(),
notifications: z.array(z.string()).optional(),
tags: z.array(z.string()).optional(),
customUpCodes: z.array(httpStatusCode).default([]),
secret: z.string().optional(),
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(),
gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
strategy: z.enum(PageSpeedStrategies).optional(),
selectedDisks: z.array(z.string()).optional(),
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
geoCheckInterval: z.number().min(300000).optional(),
dnsServer: dnsServerValidation.optional(),
dnsRecordType: z.enum(DnsRecordTypes).optional(),
})
.superRefine(refineDnsHostname)
.superRefine(refineStrategyType);
export const editMonitorBodyValidation = z
.object({
name: z.string().optional(),
type: z.enum(MonitorTypes).optional(),
url: z.string().optional(),
statusWindowSize: z.number().min(1).max(20).default(5),
statusWindowThreshold: z.number().min(1).max(100).default(60),
description: z.union([z.string(), z.literal("")]).optional(),
interval: z.number().optional(),
notifications: z.array(z.string()).optional(),
tags: z.array(z.string()).optional(),
customUpCodes: z.array(httpStatusCode).optional(),
secret: z.string().optional(),
ignoreTlsErrors: z.boolean().optional(),
useAdvancedMatching: z.boolean().optional(),
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(),
port: z.number().min(1).max(65535).optional(),
cpuAlertThreshold: z.number().optional(),
memoryAlertThreshold: z.number().optional(),
diskAlertThreshold: z.number().optional(),
tempAlertThreshold: z.number().optional(),
gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).optional(),
strategy: z.enum(PageSpeedStrategies).optional(),
selectedDisks: z.array(z.string()).optional(),
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
geoCheckInterval: z.number().min(300000).optional(),
dnsServer: dnsServerValidation.optional(),
dnsRecordType: z.enum(DnsRecordTypes).optional(),
})
.superRefine(refineDnsHostname)
.superRefine(refineStrategyType);
export const pauseMonitorParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
export const bulkPauseMonitorBodyValidation = z.object({
monitorIds: z
.array(z.string().min(1, "Monitor ID must not be empty"))
.min(1, "At least one monitor ID is required")
.max(100, "Cannot bulk update more than 100 monitors at once"),
pause: z.boolean(),
});
export const getUptimeDetailsByIdParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
export const getUptimeDetailsByIdQueryValidation = z.object({
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
normalize: booleanCoercion.optional(),
});
const importedMonitorSchema = z
.object({
id: z.string().optional(),
userId: z.string().optional(),
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"]).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().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().default(true),
interval: z.number().default(60000),
uptimePercentage: z.number().optional(),
notifications: z.array(z.string()).default([]),
tags: z.array(z.string()).default([]),
customUpCodes: z.array(httpStatusCode).default([]),
secret: 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("")]).default(""),
strategy: z.enum(PageSpeedStrategies).optional(),
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),
dnsServer: dnsServerValidation.optional(),
dnsRecordType: z.enum(DnsRecordTypes).optional(),
createdAt: z.string().optional(),
updatedAt: z.string().optional(),
})
.superRefine(refineDnsHostname)
.superRefine(refineStrategyType);
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"),
});
export const getHardwareDetailsByIdQueryValidation = z.object({
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
});
// Canonical monitor shape returned by /monitors endpoints. Keep aligned with
// what the controllers actually serialize.
export const monitorResponseSchema = z
.object({
_id: z.string(),
name: z.string(),
description: z.string().optional(),
type: z.enum(MonitorTypes),
url: z.string(),
port: z.number().optional(),
isActive: z.boolean(),
interval: z.number(),
status: z.enum(MonitorStatuses),
statusWindowSize: z.number(),
statusWindowThreshold: z.number(),
ignoreTlsErrors: z.boolean(),
useAdvancedMatching: z.boolean(),
jsonPath: z.string().optional(),
expectedValue: z.string().optional(),
matchMethod: z.enum(MonitorMatchMethods).optional(),
notifications: z.array(z.string()),
tags: z.array(z.string()),
customUpCodes: z.array(httpStatusCode).optional(),
secret: z.string().optional(),
cpuAlertThreshold: z.number(),
memoryAlertThreshold: z.number(),
diskAlertThreshold: z.number(),
tempAlertThreshold: z.number(),
selectedDisks: z.array(z.string()),
gameId: z.string().optional(),
grpcServiceName: z.string().optional(),
group: z.string().nullable().optional(),
geoCheckEnabled: z.boolean(),
geoCheckLocations: z.array(z.enum(GeoContinents)),
geoCheckInterval: z.number(),
dnsServer: z.string().optional(),
dnsRecordType: z.enum(DnsRecordTypes).optional(),
teamId: z.string(),
userId: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
})
.passthrough();
@@ -0,0 +1,148 @@
import { z } from "zod";
//****************************************
// Notification Validations
//****************************************
export const createNotificationBodyValidation = z.discriminatedUnion("type", [
// Email notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("email"),
address: z.email("Please enter a valid e-mail address"),
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
roomId: z.union([z.string(), z.literal("")]).optional(),
accessToken: z.union([z.string(), z.literal("")]).optional(),
}),
// Webhook notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("webhook"),
address: z.url({ message: "Please enter a valid Webhook URL" }),
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
roomId: z.union([z.string(), z.literal("")]).optional(),
accessToken: z.union([z.string(), z.literal("")]).optional(),
}),
// Slack notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("slack"),
address: z.url({ message: "Please enter a valid Webhook URL" }),
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
roomId: z.union([z.string(), z.literal("")]).optional(),
accessToken: z.union([z.string(), z.literal("")]).optional(),
}),
// Discord notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("discord"),
address: z.url({ message: "Please enter a valid Webhook URL" }),
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
roomId: z.union([z.string(), z.literal("")]).optional(),
accessToken: z.union([z.string(), z.literal("")]).optional(),
}),
// PagerDuty notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("pager_duty"),
address: z.string().min(1, "PagerDuty integration key is required"),
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
roomId: z.union([z.string(), z.literal("")]).optional(),
accessToken: z.union([z.string(), z.literal("")]).optional(),
}),
// Matrix notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("matrix"),
address: z.union([z.string(), z.literal("")]).optional(),
homeserverUrl: z.url({ message: "Please enter a valid Homeserver URL" }),
roomId: z.string().min(1, "Room ID is required"),
accessToken: z.string().min(1, "Access Token is required"),
}),
// Teams notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("teams"),
address: z.url({ message: "Please enter a valid Webhook URL" }),
}),
// Telegram notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("telegram"),
address: z.string().min(1, "Chat ID is required"),
accessToken: z.string().min(1, "Bot token is required"),
}),
// Pushover notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("pushover"),
address: z.string().min(1, "User key is required"),
accessToken: z.string().min(1, "App token is required"),
}),
// Twilio SMS notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("twilio"),
accountSid: z.string().min(1, "Account SID is required"),
accessToken: z.string().min(1, "Auth token is required"),
phone: z.string().min(1, "Recipient phone number is required"),
twilioPhoneNumber: z.string().min(1, "Twilio phone number is required"),
}),
// ntfy notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("ntfy"),
address: z.url({ message: "Please enter a valid ntfy server URL" }),
topic: z.string().min(1, "Topic is required"),
}),
]);
export const testNotificationBodyValidation = createNotificationBodyValidation;
export const deleteNotificationParamValidation = z.object({
id: z.string().min(1, "Notification ID is required"),
});
export const getNotificationByIdParamValidation = z.object({
id: z.string().min(1, "Notification ID is required"),
});
export const editNotificationParamValidation = z.object({
id: z.string().min(1, "Notification ID is required"),
});
export const testAllNotificationsBodyValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
export const sendTestEmailBodyValidation = z.object({
to: z.string().min(1, "To field is required"),
systemEmailHost: z.string().optional(),
systemEmailPort: z.number().optional(),
systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(),
systemEmailAddress: z.string().optional(),
systemEmailDisplayName: z.string().optional(),
systemEmailPassword: z.string().optional(),
systemEmailUser: z.string().optional(),
systemEmailConnectionHost: z.union([z.string(), z.literal("")]).optional(),
systemEmailIgnoreTLS: z.boolean().optional(),
systemEmailRequireTLS: z.boolean().optional(),
systemEmailRejectUnauthorized: z.boolean().optional(),
systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(),
});
export const updateNotificationsValidation = z
.object({
monitorIds: z.array(z.string()).min(1, "At least one monitor ID is required").max(100, "Cannot update more than 100 monitors at once"),
notificationIds: z.array(z.string()).max(100, "Cannot specify more than 100 notification IDs at once"),
action: z.enum(["add", "remove", "set"] as const),
})
.refine(
(data) => {
if (data.action !== "set" && data.notificationIds.length === 0) return false;
return true;
},
{
message: "Notification IDs cannot be empty unless action is 'set'",
path: ["notificationIds"],
}
);
@@ -0,0 +1,6 @@
import { z } from "zod";
export const getQueueJobsQueryValidation = z.object({
page: z.coerce.number().int().min(0).optional(),
rowsPerPage: z.coerce.number().int().min(1).max(100).optional(),
});
@@ -0,0 +1,44 @@
import { CHECK_TTL_SENTINEL } from "@/domain/checks/check.type.js";
import { z } from "zod";
//****************************************
// Settings Validations
//****************************************
export const updateAppSettingsBodyValidation = z
.object({
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(),
systemEmailDisplayName: z
.string()
.max(100)
.transform((val) => (val.trim() === "" ? null : val.trim()))
.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(),
systemEmailPool: z.boolean().optional(),
systemEmailIgnoreTLS: z.boolean().optional(),
systemEmailRequireTLS: z.boolean().optional(),
systemEmailRejectUnauthorized: z.boolean().optional(),
globalThresholds: z
.object({
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(),
})
.strip();
+44
View File
@@ -0,0 +1,44 @@
import { z } from "zod";
import { type UserRole } from "@/domain/users/user.type.js";
export const passwordPattern =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/])[A-Za-z0-9!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/]+$/;
export const passwordValidation = z
.string()
.min(8, "Password must be at least 8 characters")
.regex(passwordPattern, "Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character");
export const nameValidation = z
.string()
.trim()
.max(50, "Name must be less than 50 characters")
.regex(
/^(?=.*[\p{L}\p{Sc}])[\p{L}\p{Sc}\s'\-().]+$/u,
"Names must contain at least 1 letter and may only include letters, currency symbols, spaces, apostrophes, hyphens (-), periods (.), and parentheses ()."
);
export const lowercaseEmailValidation = z.email().transform((val) => val.toLowerCase());
export const dnsServerValidation = z
.string()
.min(1, "DNS server is required")
.refine((v) => z.ipv4().safeParse(v).success || z.ipv6().safeParse(v).success, "Enter a valid IPv4 or IPv6 address (e.g. 8.8.8.8)");
// Hostname (FQDN) — labels of 1-63 alphanumerics/hyphens separated by dots,
// optionally prefixed with `_` for service labels (e.g. _dmarc, _imaps._tcp).
// No scheme, port, path, or whitespace. Total length ≤ 253. Kept in sync with
// the client-side regex in client/src/Validation/monitor.ts.
export const dnsHostnameRegex = /^(?=.{1,253}$)([a-zA-Z0-9_](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;
export const booleanCoercion = z.preprocess((val) => {
if (val === "true" || val === true) return true;
if (val === "false" || val === false) return false;
return val; // Let Zod validation handle invalid values
}, z.boolean());
export const roleValidator = (allowedRoles: UserRole[]) => {
return z.array(z.custom<UserRole>()).refine((userRoles) => allowedRoles.some((role) => userRoles.includes(role)), {
message: `You do not have the required authorization. Required roles: ${allowedRoles.join(", ")}`,
});
};
@@ -0,0 +1,57 @@
import { z } from "zod";
import { booleanCoercion } from "./shared.js";
import { StatusPageTypes, StatusPageThemes, StatusPageThemeModes } from "@/domain/status-pages/status-page.type.js";
//****************************************
// Status Page Validations
//****************************************
export const getStatusPageParamValidation = z.object({
url: z.string().min(1, "URL is required"),
});
export const getStatusPageQueryValidation = z.object({
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.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",
}),
timezone: z.string().optional(),
color: z.string().optional(),
monitors: z.array(z.string().regex(/^[0-9a-fA-F]{24}$/, "Must be a valid monitor ID")).min(1, "At least one monitor is required"),
subMonitors: z.array(z.string().regex(/^[0-9a-fA-F]{24}$/)).optional(),
deleteSubmonitors: z.boolean().optional(),
isPublished: booleanCoercion,
showCharts: booleanCoercion.optional(),
showUptimePercentage: booleanCoercion,
showAdminLoginLink: booleanCoercion.optional(),
showInfrastructure: booleanCoercion.optional(),
removeLogo: z.union([z.literal("true"), z.literal("false")]).optional(),
theme: z.enum(StatusPageThemes).optional(),
themeMode: z.enum(StatusPageThemeModes).optional(),
})
.strip();
export const imageValidation = z
.object({
fieldname: z.string().min(1, "Field name is required"),
originalname: z.string().min(1, "Original name is required"),
encoding: z.string().min(1, "Encoding is required"),
mimetype: z.enum(["image/jpeg", "image/png", "image/jpg"], {
message: "File must be a valid image (jpeg, jpg, or png)",
}),
size: z.number().max(3145728, "File size must be less than 3MB"),
buffer: z.instanceof(Buffer, { message: "Buffer is required" }),
destination: z.string().optional(),
filename: z.string().optional(),
path: z.string().optional(),
})
.refine((data) => data.buffer, {
message: "Image file is required",
});
@@ -0,0 +1,25 @@
import { z } from "zod";
//****************************************
// Tag Validations
//****************************************
export const createTagBodyValidation = z.object({
name: z.string().min(1, "Tag name is required"),
color: z
.string()
.regex(/^#([0-9a-fA-F]{6})$/, "Color must be a 6-digit hex value (e.g. #13715B)")
.optional(),
});
export const editTagBodyValidation = createTagBodyValidation;
export const getTagByIdParamValidation = z.object({
id: z.string().min(1, "Tag ID is required"),
});
export const editTagParamValidation = z.object({
id: z.string().min(1, "Tag ID is required"),
});
export const deleteTagParamValidation = z.object({
id: z.string().min(1, "Tag ID is required"),
});
@@ -0,0 +1,72 @@
import { z } from "zod";
import { UserRoles } from "@/domain/users/user.type.js";
import { nameValidation, lowercaseEmailValidation, passwordValidation } from "./shared.js";
//****************************************
// User Validations
//****************************************
export const getUserByIdParamValidation = z.object({
userId: z.string().min(1, "User ID is required"),
});
export const editUserByIdParamValidation = z.object({
userId: z.string().min(1, "User ID is required"),
});
export const editUserByIdBodyValidation = z.object({
firstName: nameValidation,
lastName: nameValidation,
role: z.array(z.enum(UserRoles)).min(1, "At least one role is required"),
});
export const editSuperadminUserByIdBodyValidation = z.object({
firstName: nameValidation,
lastName: nameValidation,
role: z.array(z.enum(UserRoles)).min(1, "At least one role is required"),
});
export const editUserPasswordByIdBodyValidation = z.object({
password: passwordValidation,
});
export const createUserBodyValidation = z.object({
firstName: nameValidation,
lastName: nameValidation,
email: lowercaseEmailValidation,
password: passwordValidation,
role: z.array(z.enum(UserRoles)).min(1, "At least one role is required"),
});
export const editUserBodyValidation = z
.object({
firstName: nameValidation.optional(),
lastName: nameValidation.optional(),
email: lowercaseEmailValidation.optional(),
profilePicture: z.string().optional(),
password: passwordValidation.optional(),
newPassword: passwordValidation.optional(),
})
// Changing a password requires both the current password and the new one.
// The service only runs the change-password flow when both are present, so
// reject a half-filled request
.refine((data) => (data.password === undefined) === (data.newPassword === undefined), {
message: "Both current password and new password are required to change your password",
path: ["newPassword"],
});
// Canonical user shape returned by auth/user endpoints. Keep aligned with what
// the controllers actually serialize (password is intentionally omitted).
export const userResponseSchema = z
.object({
_id: z.string(),
firstName: z.string(),
lastName: z.string(),
email: z.string(),
role: z.array(z.string()),
teamId: z.string().optional(),
profileImage: z.string().nullable().optional(),
createdAt: z.string(),
updatedAt: z.string(),
})
.passthrough();