Files
Checkmate/server/src/api/validation/notificationValidation.ts
T

197 lines
7.7 KiB
TypeScript
Raw Normal View History

2026-03-02 17:29:48 +00:00
import { z } from "zod";
import { NtfyAuthTypes } from "@/domain/notifications/notification.type.js";
2026-03-01 17:35:00 +00:00
//****************************************
// Notification Validations
//****************************************
// ntfy stores its secret in accessToken for both auth types: the bearer token for
// "token", the password for "basic". Reject partially filled credentials so a channel
// can't be saved looking authenticated while posting anonymously.
const refineNtfyAuth = (body: { ntfyAuthType?: string; ntfyUsername?: string; accessToken?: string }, ctx: z.RefinementCtx) => {
const authType = body.ntfyAuthType ?? "none";
if (authType === "none") {
if (body.accessToken) {
ctx.addIssue({ code: "custom", path: ["accessToken"], message: "Select an authentication type to use an access token" });
}
if (body.ntfyUsername) {
ctx.addIssue({ code: "custom", path: ["ntfyUsername"], message: "Select an authentication type to use a username" });
}
return;
}
if (authType === "token") {
if (!body.accessToken) {
ctx.addIssue({ code: "custom", path: ["accessToken"], message: "Access token is required for token authentication" });
}
if (body.ntfyUsername) {
ctx.addIssue({ code: "custom", path: ["ntfyUsername"], message: "Username is only used with basic authentication" });
}
return;
}
if (!body.ntfyUsername) {
ctx.addIssue({ code: "custom", path: ["ntfyUsername"], message: "Username is required for basic authentication" });
}
if (!body.accessToken) {
ctx.addIssue({ code: "custom", path: ["accessToken"], message: "Password is required for basic authentication" });
}
};
2026-03-02 19:42:47 +00:00
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(),
2026-03-01 17:35:00 +00:00
}),
2026-03-02 19:42:47 +00:00
// Webhook notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("webhook"),
2026-03-09 08:43:14 +11:00
address: z.url({ message: "Please enter a valid Webhook URL" }),
2026-03-02 19:42:47 +00:00
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
roomId: z.union([z.string(), z.literal("")]).optional(),
accessToken: z.union([z.string(), z.literal("")]).optional(),
2026-03-01 17:35:00 +00:00
}),
// Rocket.Chat notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("rocket_chat"),
2026-08-05 08:34:12 -03:00
address: z.url({
protocol: /^https?$/,
message: "Please enter a valid Rocket.Chat webhook URL",
}),
}),
2026-03-02 19:42:47 +00:00
// Slack notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("slack"),
2026-03-09 08:43:14 +11:00
address: z.url({ message: "Please enter a valid Webhook URL" }),
2026-03-02 19:42:47 +00:00
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
roomId: z.union([z.string(), z.literal("")]).optional(),
accessToken: z.union([z.string(), z.literal("")]).optional(),
2026-03-01 17:35:00 +00:00
}),
2026-03-02 19:42:47 +00:00
// Discord notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("discord"),
2026-03-09 08:43:14 +11:00
address: z.url({ message: "Please enter a valid Webhook URL" }),
2026-03-02 19:42:47 +00:00
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
roomId: z.union([z.string(), z.literal("")]).optional(),
accessToken: z.union([z.string(), z.literal("")]).optional(),
2026-03-01 17:35:00 +00:00
}),
2026-03-02 19:42:47 +00:00
// 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(),
2026-03-01 17:35:00 +00:00
}),
2026-03-02 19:42:47 +00:00
// 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(),
2026-03-09 08:43:14 +11:00
homeserverUrl: z.url({ message: "Please enter a valid Homeserver URL" }),
2026-03-02 19:42:47 +00:00
roomId: z.string().min(1, "Room ID is required"),
accessToken: z.string().min(1, "Access Token is required"),
2026-03-01 17:35:00 +00:00
}),
2026-03-05 19:16:08 +11:00
// Teams notification
z.object({
notificationName: z.string().min(1, "Notification name is required"),
type: z.literal("teams"),
2026-03-09 08:43:14 +11:00
address: z.url({ message: "Please enter a valid Webhook URL" }),
2026-03-05 19:16:08 +11:00
}),
// 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"),
}),
2026-04-16 16:25:24 +03:00
// 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"),
}),
2026-04-16 21:46:40 +03:00
// 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"),
2026-04-16 21:46:40 +03:00
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"),
2026-04-16 21:46:40 +03:00
}),
2026-05-27 16:57:11 +01:00
// 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"),
ntfyAuthType: z.enum(NtfyAuthTypes).optional(),
ntfyUsername: z.union([z.string(), z.literal("")]).optional(),
accessToken: z.union([z.string(), z.literal("")]).optional(),
})
.superRefine(refineNtfyAuth),
2026-03-02 19:42:47 +00:00
]);
2026-03-02 17:29:48 +00:00
2026-03-11 13:28:48 -07:00
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"),
});
2026-03-02 17:29:48 +00:00
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(),
2026-04-29 12:57:37 -07:00
systemEmailDisplayName: z.string().optional(),
2026-03-02 17:29:48 +00:00
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({
2026-03-23 10:13:43 -07:00
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"],
}
);