reorg to domain and api
This commit is contained in:
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(),
|
||||
});
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
Reference in new issue
Block a user