add validation

This commit is contained in:
Alex Holliday committed 2026-08-28 13:59:24 -07:00
1 parent 4ec03dfd04
commit cacedfd7d8
1 file changed
+89 -3
+89 -3
View File
@@ -13,6 +13,7 @@ import {
ProxyModes,
} from "@/domain/monitors/monitor.type.js";
import { DateRanges, SortOrders } from "@/types/query.js";
import { DockerContainerStates, DockerHealthStatuses } from "@/types/network.js";
const httpStatusCode = z.number().refine((code) => HttpStatusCodeSet.has(code), { message: "Must be a valid HTTP status code" });
@@ -118,6 +119,26 @@ const refineProxySelection = (body: { proxyMode?: string; proxyId?: string }, ct
}
};
const dockerUrlRegex = /^(?:ssh:\/\/[^@\s]+@[^\s/:@]+(?::\d{1,5})?\/?|unix:\/\/\/\S+|\/\S+)$/;
const refineDockerUrl = (data: { type?: string; url?: string; sshPrivateKey?: string }, ctx: z.RefinementCtx) => {
if (data.type !== "docker" || data.url === undefined) return;
if (!dockerUrlRegex.test(data.url)) {
ctx.addIssue({
code: "custom",
path: ["url"],
message: "Docker host must be ssh://user@host[:port], unix:///path, or an absolute socket path",
});
}
if (data.url.startsWith("ssh://") && !data.sshPrivateKey) {
ctx.addIssue({
code: "custom",
path: ["sshPrivateKey"],
message: "SSH Docker hosts require a private key",
});
}
};
export const createMonitorBodyValidation = z
.object({
_id: z.string().optional(),
@@ -142,6 +163,7 @@ export const createMonitorBodyValidation = z
tags: z.array(z.string()).optional(),
customUpCodes: z.array(httpStatusCode).default([]),
secret: z.string().optional(),
sshPrivateKey: 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(),
@@ -161,7 +183,8 @@ export const createMonitorBodyValidation = z
.superRefine(refineStrategyType)
.superRefine(refineHeadMatching)
.superRefine(refineRegexPattern)
.superRefine(refineProxySelection);
.superRefine(refineProxySelection)
.superRefine(refineDockerUrl);
export const editMonitorBodyValidation = z
.object({
@@ -176,6 +199,7 @@ export const editMonitorBodyValidation = z
tags: z.array(z.string()).optional(),
customUpCodes: z.array(httpStatusCode).optional(),
secret: z.string().optional(),
sshPrivateKey: z.string().optional(),
ignoreTlsErrors: z.boolean().optional(),
proxyMode: z.enum(ProxyModes).optional(),
proxyId: proxyIdValidation,
@@ -204,7 +228,8 @@ export const editMonitorBodyValidation = z
.superRefine(refineStrategyType)
.superRefine(refineHeadMatching)
.superRefine(refineRegexPattern)
.superRefine(refineProxySelection);
.superRefine(refineProxySelection)
.superRefine(refineDockerUrl);
export const pauseMonitorParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
@@ -255,6 +280,7 @@ const importedMonitorSchema = z
tags: z.array(z.string()).default([]),
customUpCodes: z.array(httpStatusCode).default([]),
secret: z.string().optional(),
sshPrivateKey: z.string().optional(),
cpuAlertThreshold: z.number().default(100),
cpuAlertCounter: z.number().default(5),
memoryAlertThreshold: z.number().default(100),
@@ -280,7 +306,8 @@ const importedMonitorSchema = z
.superRefine(refineStrategyType)
.superRefine(refineHeadMatching)
.superRefine(refineRegexPattern)
.superRefine(refineProxySelection);
.superRefine(refineProxySelection)
.superRefine(refineDockerUrl);
export const importMonitorsBodyValidation = z.object({
monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"),
@@ -296,6 +323,9 @@ export const getHardwareDetailsByIdQueryValidation = z.object({
dateRange: z.enum(DateRanges).optional(),
});
export const getDockerDetailsByIdParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required") });
export const getDockerDetailsByIdQueryValidation = z.object({ dateRange: z.enum(DateRanges).optional() });
// Canonical monitor shape returned by /monitors endpoints. Keep aligned with
// what the controllers actually serialize.
export const monitorResponseSchema = z
@@ -323,6 +353,7 @@ export const monitorResponseSchema = z
tags: z.array(z.string()),
customUpCodes: z.array(httpStatusCode).optional(),
secret: z.string().optional(),
sshPrivateKey: z.string().optional(),
cpuAlertThreshold: z.number(),
memoryAlertThreshold: z.number(),
diskAlertThreshold: z.number(),
@@ -392,3 +423,58 @@ export const uptimeDetailsResponseSchema = z.object({
}),
monitorStats: monitorStatsResponseSchema.nullable(),
});
// Keep aligned with DockerContainerInfo / DockerContainerSummary in types/network.ts.
export const dockerContainerResponseSchema = z.object({
id: z.string(),
name: z.string(),
image: z.string(),
state: z.enum(DockerContainerStates),
status: z.string(),
health: z.enum(DockerHealthStatuses),
cpuPct: z.number().optional(),
memoryUsedBytes: z.number().optional(),
memoryLimitBytes: z.number().optional(),
memoryPct: z.number().optional(),
restartCount: z.number().optional(),
startedAt: z.string().optional(),
});
export const containerSummaryResponseSchema = z.object({
total: z.number(),
running: z.number(),
stopped: z.number(),
unhealthy: z.number(),
});
// Keep aligned with DockerStatsBucket in domain/checks/check.type.ts. The avg fields are
// null for buckets with no values ($avg skips missing; down checks store no containerSummary).
export const dockerStatsBucketResponseSchema = z.object({
_id: z.string(),
avgResponseTime: z.number().nullable(),
upCount: z.number(),
totalCount: z.number(),
avgRunning: z.number().nullable(),
avgTotal: z.number().nullable(),
avgUnhealthy: z.number().nullable(),
});
// Response of GET /monitors/docker/details/{monitorId}. Keep aligned with
// DockerDetailsResult in domain/monitors/monitor.type.ts; the monitor here is the
// repository's domain entity, which serializes `id` rather than `_id`.
export const dockerDetailsResponseSchema = z.object({
monitor: monitorResponseSchema.omit({ _id: true }).extend({ id: z.string() }),
stats: z.object({
aggregateData: z.object({ totalChecks: z.number() }),
upChecks: z.object({ totalChecks: z.number() }),
aggregate: z.array(dockerStatsBucketResponseSchema),
latest: z
.object({
containers: z.array(dockerContainerResponseSchema),
summary: containerSummaryResponseSchema.optional(),
checkedAt: z.string(),
})
.nullable(),
}),
monitorStats: monitorStatsResponseSchema.nullable(),
});