From ad382f1fe53cf2d78bb4e66c8e4c0425d0934fbf Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:38:52 +0000 Subject: [PATCH] [autofix.ci] apply automated fixes --- .../notifications/handle-notifications.tsx | 3778 ++++++++--------- .../components/icons/notification-icons.tsx | 558 +-- .../server/api/routers/notification.ts | 1412 +++--- packages/server/src/services/notification.ts | 1976 ++++----- .../server/src/utils/notifications/utils.ts | 554 +-- .../src/utils/notifications/volume-backup.ts | 576 ++- 6 files changed, 4423 insertions(+), 4431 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index b328e3c8f..7b477c92b 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -1,16 +1,16 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { - AlertTriangle, - Mail, - PenBoxIcon, - PlusIcon, - Trash2, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; -import { +import { zodResolver } from "@hookform/resolvers/zod"; +import { + AlertTriangle, + Mail, + PenBoxIcon, + PlusIcon, + Trash2, +} from "lucide-react"; +import { useEffect, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { DiscordIcon, GotifyIcon, LarkIcon, @@ -20,1879 +20,1879 @@ import { SlackIcon, TelegramIcon, } from "@/components/icons/notification-icons"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { Switch } from "@/components/ui/switch"; -import { api } from "@/utils/api"; - -const notificationBaseSchema = z.object({ - name: z.string().min(1, { - message: "Name is required", - }), - appDeploy: z.boolean().default(false), - appBuildError: z.boolean().default(false), - databaseBackup: z.boolean().default(false), - volumeBackup: z.boolean().default(false), - dokployRestart: z.boolean().default(false), - dockerCleanup: z.boolean().default(false), - serverThreshold: z.boolean().default(false), -}); - -export const notificationSchema = z.discriminatedUnion("type", [ - z - .object({ - type: z.literal("slack"), - webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), - channel: z.string(), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("telegram"), - botToken: z.string().min(1, { message: "Bot Token is required" }), - chatId: z.string().min(1, { message: "Chat ID is required" }), - messageThreadId: z.string().optional(), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("discord"), - webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), - decoration: z.boolean().default(true), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("email"), - smtpServer: z.string().min(1, { message: "SMTP Server is required" }), - smtpPort: z.number().min(1, { message: "SMTP Port is required" }), - username: z.string().min(1, { message: "Username is required" }), - password: z.string().min(1, { message: "Password is required" }), - fromAddress: z.string().min(1, { message: "From Address is required" }), - toAddresses: z - .array( - z.string().min(1, { message: "Email is required" }).email({ - message: "Email is invalid", - }), - ) - .min(1, { message: "At least one email is required" }), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("resend"), - apiKey: z.string().min(1, { message: "API Key is required" }), - fromAddress: z - .string() - .min(1, { message: "From Address is required" }) - .email({ message: "Email is invalid" }), - toAddresses: z - .array( - z.string().min(1, { message: "Email is required" }).email({ - message: "Email is invalid", - }), - ) - .min(1, { message: "At least one email is required" }), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("gotify"), - serverUrl: z.string().min(1, { message: "Server URL is required" }), - appToken: z.string().min(1, { message: "App Token is required" }), - priority: z.number().min(1).max(10).default(5), - decoration: z.boolean().default(true), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("ntfy"), - serverUrl: z.string().min(1, { message: "Server URL is required" }), - topic: z.string().min(1, { message: "Topic is required" }), - accessToken: z.string().optional(), - priority: z.number().min(1).max(5).default(3), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("pushover"), - userKey: z.string().min(1, { message: "User Key is required" }), - apiToken: z.string().min(1, { message: "API Token is required" }), - priority: z.number().min(-2).max(2).default(0), - retry: z.number().min(30).nullish(), - expire: z.number().min(1).max(10800).nullish(), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("custom"), - endpoint: z.string().min(1, { message: "Endpoint URL is required" }), - headers: z - .array( - z.object({ - key: z.string(), - value: z.string(), - }), - ) - .optional() - .default([]), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("lark"), - webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), - }) - .merge(notificationBaseSchema), -]); - -export const notificationsMap = { - slack: { - icon: , - label: "Slack", - }, - telegram: { - icon: , - label: "Telegram", - }, - discord: { - icon: , - label: "Discord", - }, - lark: { - icon: , - label: "Lark", - }, - email: { - icon: , - label: "Email", - }, - resend: { - icon: , - label: "Resend", - }, - gotify: { - icon: , - label: "Gotify", - }, - ntfy: { - icon: , - label: "ntfy", - }, - pushover: { - icon: , - label: "Pushover", - }, - custom: { - icon: , - label: "Custom", - }, -}; - -export type NotificationSchema = z.infer; - -interface Props { - notificationId?: string; -} - -export const HandleNotifications = ({ notificationId }: Props) => { - const utils = api.useUtils(); - const [visible, setVisible] = useState(false); - const { data: isCloud } = api.settings.isCloud.useQuery(); - - const { data: notification } = api.notification.one.useQuery( - { - notificationId: notificationId || "", - }, - { - enabled: !!notificationId, - }, - ); - const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } = - api.notification.testSlackConnection.useMutation(); - const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } = - api.notification.testTelegramConnection.useMutation(); - const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } = - api.notification.testDiscordConnection.useMutation(); - const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } = - api.notification.testEmailConnection.useMutation(); - const { mutateAsync: testResendConnection, isLoading: isLoadingResend } = - api.notification.testResendConnection.useMutation(); - const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } = - api.notification.testGotifyConnection.useMutation(); - const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } = - api.notification.testNtfyConnection.useMutation(); - const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } = - api.notification.testLarkConnection.useMutation(); - - const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } = - api.notification.testCustomConnection.useMutation(); - - const { mutateAsync: testPushoverConnection, isLoading: isLoadingPushover } = - api.notification.testPushoverConnection.useMutation(); - - const customMutation = notificationId - ? api.notification.updateCustom.useMutation() - : api.notification.createCustom.useMutation(); - const slackMutation = notificationId - ? api.notification.updateSlack.useMutation() - : api.notification.createSlack.useMutation(); - const telegramMutation = notificationId - ? api.notification.updateTelegram.useMutation() - : api.notification.createTelegram.useMutation(); - const discordMutation = notificationId - ? api.notification.updateDiscord.useMutation() - : api.notification.createDiscord.useMutation(); - const emailMutation = notificationId - ? api.notification.updateEmail.useMutation() - : api.notification.createEmail.useMutation(); - const resendMutation = notificationId - ? api.notification.updateResend.useMutation() - : api.notification.createResend.useMutation(); - const gotifyMutation = notificationId - ? api.notification.updateGotify.useMutation() - : api.notification.createGotify.useMutation(); - const ntfyMutation = notificationId - ? api.notification.updateNtfy.useMutation() - : api.notification.createNtfy.useMutation(); - const larkMutation = notificationId - ? api.notification.updateLark.useMutation() - : api.notification.createLark.useMutation(); - const pushoverMutation = notificationId - ? api.notification.updatePushover.useMutation() - : api.notification.createPushover.useMutation(); - - const form = useForm({ - defaultValues: { - type: "slack", - webhookUrl: "", - channel: "", - name: "", - }, - resolver: zodResolver(notificationSchema), - }); - const type = form.watch("type"); - - const { fields, append, remove } = useFieldArray({ - control: form.control, - name: "toAddresses" as never, - }); - - const { - fields: headerFields, - append: appendHeader, - remove: removeHeader, - } = useFieldArray({ - control: form.control, - name: "headers" as never, - }); - - useEffect(() => { - if ((type === "email" || type === "resend") && fields.length === 0) { - append(""); - } - }, [type, append, fields.length]); - - useEffect(() => { - if (notification) { - if (notification.notificationType === "slack") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - dockerCleanup: notification.dockerCleanup, - webhookUrl: notification.slack?.webhookUrl, - channel: notification.slack?.channel || "", - name: notification.name, - type: notification.notificationType, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "telegram") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - botToken: notification.telegram?.botToken, - messageThreadId: notification.telegram?.messageThreadId || "", - chatId: notification.telegram?.chatId, - type: notification.notificationType, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "discord") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - type: notification.notificationType, - webhookUrl: notification.discord?.webhookUrl, - decoration: notification.discord?.decoration || undefined, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "email") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - type: notification.notificationType, - smtpServer: notification.email?.smtpServer, - smtpPort: notification.email?.smtpPort, - username: notification.email?.username, - password: notification.email?.password, - toAddresses: notification.email?.toAddresses, - fromAddress: notification.email?.fromAddress, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "resend") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - type: notification.notificationType, - apiKey: notification.resend?.apiKey, - toAddresses: notification.resend?.toAddresses, - fromAddress: notification.resend?.fromAddress, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "gotify") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - type: notification.notificationType, - appToken: notification.gotify?.appToken, - decoration: notification.gotify?.decoration || undefined, - priority: notification.gotify?.priority, - serverUrl: notification.gotify?.serverUrl, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - }); - } else if (notification.notificationType === "ntfy") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - type: notification.notificationType, - accessToken: notification.ntfy?.accessToken || "", - topic: notification.ntfy?.topic, - priority: notification.ntfy?.priority, - serverUrl: notification.ntfy?.serverUrl, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "lark") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - type: notification.notificationType, - webhookUrl: notification.lark?.webhookUrl, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - volumeBackup: notification.volumeBackup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "custom") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - type: notification.notificationType, - endpoint: notification.custom?.endpoint || "", - headers: notification.custom?.headers - ? Object.entries(notification.custom.headers).map( - ([key, value]) => ({ - key, - value, - }), - ) - : [], - name: notification.name, - volumeBackup: notification.volumeBackup, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "pushover") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - type: notification.notificationType, - userKey: notification.pushover?.userKey, - apiToken: notification.pushover?.apiToken, - priority: notification.pushover?.priority, - retry: notification.pushover?.retry ?? undefined, - expire: notification.pushover?.expire ?? undefined, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } - } else { - form.reset(); - } - }, [form, form.reset, form.formState.isSubmitSuccessful, notification]); - - const activeMutation = { - slack: slackMutation, - telegram: telegramMutation, - discord: discordMutation, - email: emailMutation, - resend: resendMutation, - gotify: gotifyMutation, - ntfy: ntfyMutation, - lark: larkMutation, - custom: customMutation, - pushover: pushoverMutation, - }; - - const onSubmit = async (data: NotificationSchema) => { - const { - appBuildError, - appDeploy, - dokployRestart, - databaseBackup, - volumeBackup, - dockerCleanup, - serverThreshold, - } = data; - let promise: Promise | null = null; - if (data.type === "slack") { - promise = slackMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - webhookUrl: data.webhookUrl, - channel: data.channel, - name: data.name, - dockerCleanup: dockerCleanup, - slackId: notification?.slackId || "", - notificationId: notificationId || "", - serverThreshold: serverThreshold, - }); - } else if (data.type === "telegram") { - promise = telegramMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - botToken: data.botToken, - messageThreadId: data.messageThreadId || "", - chatId: data.chatId, - name: data.name, - dockerCleanup: dockerCleanup, - notificationId: notificationId || "", - telegramId: notification?.telegramId || "", - serverThreshold: serverThreshold, - }); - } else if (data.type === "discord") { - promise = discordMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - webhookUrl: data.webhookUrl, - decoration: data.decoration, - name: data.name, - dockerCleanup: dockerCleanup, - notificationId: notificationId || "", - discordId: notification?.discordId || "", - serverThreshold: serverThreshold, - }); - } else if (data.type === "email") { - promise = emailMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - smtpServer: data.smtpServer, - smtpPort: data.smtpPort, - username: data.username, - password: data.password, - fromAddress: data.fromAddress, - toAddresses: data.toAddresses, - name: data.name, - dockerCleanup: dockerCleanup, - notificationId: notificationId || "", - emailId: notification?.emailId || "", - serverThreshold: serverThreshold, - }); - } else if (data.type === "resend") { - promise = resendMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - apiKey: data.apiKey, - fromAddress: data.fromAddress, - toAddresses: data.toAddresses, - name: data.name, - dockerCleanup: dockerCleanup, - notificationId: notificationId || "", - resendId: notification?.resendId || "", - serverThreshold: serverThreshold, - }); - } else if (data.type === "gotify") { - promise = gotifyMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - serverUrl: data.serverUrl, - appToken: data.appToken, - priority: data.priority, - name: data.name, - dockerCleanup: dockerCleanup, - decoration: data.decoration, - notificationId: notificationId || "", - gotifyId: notification?.gotifyId || "", - }); - } else if (data.type === "ntfy") { - promise = ntfyMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - serverUrl: data.serverUrl, - accessToken: data.accessToken || "", - topic: data.topic, - priority: data.priority, - name: data.name, - dockerCleanup: dockerCleanup, - notificationId: notificationId || "", - ntfyId: notification?.ntfyId || "", - }); - } else if (data.type === "lark") { - promise = larkMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - webhookUrl: data.webhookUrl, - name: data.name, - dockerCleanup: dockerCleanup, - notificationId: notificationId || "", - larkId: notification?.larkId || "", - serverThreshold: serverThreshold, - }); - } else if (data.type === "custom") { - // Convert headers array to object - const headersRecord = - data.headers && data.headers.length > 0 - ? data.headers.reduce( - (acc, { key, value }) => { - if (key.trim()) acc[key] = value; - return acc; - }, - {} as Record, - ) - : undefined; - - promise = customMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - endpoint: data.endpoint, - headers: headersRecord, - name: data.name, - dockerCleanup: dockerCleanup, - serverThreshold: serverThreshold, - notificationId: notificationId || "", - customId: notification?.customId || "", - }); - } else if (data.type === "pushover") { - if (data.priority === 2 && (data.retry == null || data.expire == null)) { - toast.error("Retry and expire are required for emergency priority (2)"); - return; - } - promise = pushoverMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - userKey: data.userKey, - apiToken: data.apiToken, - priority: data.priority, - retry: data.priority === 2 ? data.retry : undefined, - expire: data.priority === 2 ? data.expire : undefined, - name: data.name, - dockerCleanup: dockerCleanup, - serverThreshold: serverThreshold, - notificationId: notificationId || "", - pushoverId: notification?.pushoverId || "", - }); - } - - if (promise) { - await promise - .then(async () => { - toast.success( - notificationId ? "Notification Updated" : "Notification Created", - ); - form.reset({ - type: "slack", - webhookUrl: "", - }); - setVisible(false); - await utils.notification.all.invalidate(); - }) - .catch(() => { - toast.error( - notificationId - ? "Error updating a notification" - : "Error creating a notification", - ); - }); - } - }; - return ( - - - {notificationId ? ( - - ) : ( - - )} - - - - - {notificationId ? "Update" : "Add"} Notification - - - {notificationId - ? "Update your notification providers for multiple channels." - : "Create new notification providers for multiple channels."} - - -
- - ( - - - Select a provider - - - - {Object.entries(notificationsMap).map(([key, value]) => ( - - -
- - -
-
-
- ))} -
-
- - {activeMutation[field.value].isError && ( -
- - - {activeMutation[field.value].error?.message} - -
- )} -
- )} - /> - -
- - Fill the next fields. - -
- ( - - Name - - - - - - - )} - /> - - {type === "slack" && ( - <> - ( - - Webhook URL - - - - - - - )} - /> - - ( - - Channel - - - - - - - )} - /> - - )} - - {type === "telegram" && ( - <> - ( - - Bot Token - - - - - - - )} - /> - - ( - - Chat ID - - - - - - )} - /> - - ( - - Message Thread ID - - - - - - - Optional. Use it when you want to send notifications - to a specific topic in a group. - - - )} - /> - - )} - - {type === "discord" && ( - <> - ( - - Webhook URL - - - - - - - )} - /> - - ( - -
- Decoration - - Decorate the notification with emojis. - -
- - - -
- )} - /> - - )} - - {type === "email" && ( - <> -
- ( - - SMTP Server - - - - - - - )} - /> - ( - - SMTP Port - - { - const value = e.target.value; - if (value === "") { - field.onChange(undefined); - } else { - const port = Number.parseInt(value); - if (port > 0 && port < 65536) { - field.onChange(port); - } - } - }} - value={field.value || ""} - type="number" - /> - - - - - )} - /> -
- -
- ( - - Username - - - - - - - )} - /> - - ( - - Password - - - - - - - )} - /> -
- - ( - - From Address - - - - - - )} - /> -
- To Addresses - - {fields.map((field, index) => ( -
- ( - - - - - - - - )} - /> - -
- ))} - {type === "email" && - "toAddresses" in form.formState.errors && ( -
- {form.formState?.errors?.toAddresses?.root?.message} -
- )} -
- - - - )} - - {type === "resend" && ( - <> - ( - - API Key - - - - - - )} - /> - - ( - - From Address - - - - - - )} - /> - -
- To Addresses - - {fields.map((field, index) => ( -
- ( - - - - - - - - )} - /> - -
- ))} - {type === "resend" && - "toAddresses" in form.formState.errors && ( -
- {form.formState?.errors?.toAddresses?.root?.message} -
- )} -
- - - - )} - - {type === "gotify" && ( - <> - ( - - Server URL - - - - - - )} - /> - ( - - App Token - - - - - - )} - /> - ( - - Priority - - { - const value = e.target.value; - if (value) { - const port = Number.parseInt(value); - if (port > 0 && port < 10) { - field.onChange(port); - } - } - }} - type="number" - /> - - - Message priority (1-10, default: 5) - - - - )} - /> - ( - -
- Decoration - - Decorate the notification with emojis. - -
- - - -
- )} - /> - - )} - - {type === "ntfy" && ( - <> - ( - - Server URL - - - - - - )} - /> - ( - - Topic - - - - - - )} - /> - ( - - Access Token - - - - - Optional. Leave blank for public topics. - - - - )} - /> - ( - - Priority - - { - const value = e.target.value; - if (value) { - const port = Number.parseInt(value); - if (port > 0 && port <= 5) { - field.onChange(port); - } - } - }} - type="number" - /> - - - Message priority (1-5, default: 3) - - - - )} - /> - - )} - {type === "custom" && ( -
- ( - - Webhook URL - - - - - The URL where POST requests will be sent with - notification data. - - - - )} - /> - -
-
- Headers - - Optional. Custom headers for your POST request (e.g., - Authorization, Content-Type). - -
- -
- {headerFields.map((field, index) => ( -
- ( - - - - - - )} - /> - ( - - - - - - )} - /> - -
- ))} -
- - -
-
- )} - {type === "lark" && ( - <> - ( - - Webhook URL - - - - - - )} - /> - - )} - {type === "pushover" && ( - <> - ( - - User Key - - - - - - )} - /> - ( - - API Token - - - - - - )} - /> - ( - - Priority - - { - const value = e.target.value; - if (value === "" || value === "-") { - field.onChange(0); - } else { - const priority = Number.parseInt(value); - if ( - !Number.isNaN(priority) && - priority >= -2 && - priority <= 2 - ) { - field.onChange(priority); - } - } - }} - type="number" - min={-2} - max={2} - /> - - - Message priority (-2 to 2, default: 0, emergency: 2) - - - - )} - /> - {form.watch("priority") === 2 && ( - <> - ( - - Retry (seconds) - - { - const value = e.target.value; - if (value === "") { - field.onChange(undefined); - } else { - const retry = Number.parseInt(value); - if (!Number.isNaN(retry)) { - field.onChange(retry); - } - } - }} - type="number" - min={30} - /> - - - How often (in seconds) to retry. Minimum 30 - seconds. - - - - )} - /> - ( - - Expire (seconds) - - { - const value = e.target.value; - if (value === "") { - field.onChange(undefined); - } else { - const expire = Number.parseInt(value); - if (!Number.isNaN(expire)) { - field.onChange(expire); - } - } - }} - type="number" - min={1} - max={10800} - /> - - - How long to keep retrying (max 10800 seconds / 3 - hours). - - - - )} - /> - - )} - - )} -
-
-
- - Select the actions. - - -
- ( - -
- App Deploy - - Trigger the action when a app is deployed. - -
- - - -
- )} - /> - ( - -
- App Build Error - - Trigger the action when the build fails. - -
- - - -
- )} - /> - - ( - -
- Database Backup - - Trigger the action when a database backup is created. - -
- - - -
- )} - /> - - ( - -
- Volume Backup - - Trigger the action when a volume backup is created. - -
- - - -
- )} - /> - - ( - -
- Docker Cleanup - - Trigger the action when the docker cleanup is - performed. - -
- - - -
- )} - /> - - {!isCloud && ( - ( - -
- Dokploy Restart - - Trigger the action when dokploy is restarted. - -
- - - -
- )} - /> - )} - - {isCloud && ( - ( - -
- Server Threshold - - Trigger the action when the server threshold is - reached. - -
- - - -
- )} - /> - )} -
-
- - - - - - - -
-
- ); -}; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; + +const notificationBaseSchema = z.object({ + name: z.string().min(1, { + message: "Name is required", + }), + appDeploy: z.boolean().default(false), + appBuildError: z.boolean().default(false), + databaseBackup: z.boolean().default(false), + volumeBackup: z.boolean().default(false), + dokployRestart: z.boolean().default(false), + dockerCleanup: z.boolean().default(false), + serverThreshold: z.boolean().default(false), +}); + +export const notificationSchema = z.discriminatedUnion("type", [ + z + .object({ + type: z.literal("slack"), + webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), + channel: z.string(), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("telegram"), + botToken: z.string().min(1, { message: "Bot Token is required" }), + chatId: z.string().min(1, { message: "Chat ID is required" }), + messageThreadId: z.string().optional(), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("discord"), + webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), + decoration: z.boolean().default(true), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("email"), + smtpServer: z.string().min(1, { message: "SMTP Server is required" }), + smtpPort: z.number().min(1, { message: "SMTP Port is required" }), + username: z.string().min(1, { message: "Username is required" }), + password: z.string().min(1, { message: "Password is required" }), + fromAddress: z.string().min(1, { message: "From Address is required" }), + toAddresses: z + .array( + z.string().min(1, { message: "Email is required" }).email({ + message: "Email is invalid", + }), + ) + .min(1, { message: "At least one email is required" }), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("resend"), + apiKey: z.string().min(1, { message: "API Key is required" }), + fromAddress: z + .string() + .min(1, { message: "From Address is required" }) + .email({ message: "Email is invalid" }), + toAddresses: z + .array( + z.string().min(1, { message: "Email is required" }).email({ + message: "Email is invalid", + }), + ) + .min(1, { message: "At least one email is required" }), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("gotify"), + serverUrl: z.string().min(1, { message: "Server URL is required" }), + appToken: z.string().min(1, { message: "App Token is required" }), + priority: z.number().min(1).max(10).default(5), + decoration: z.boolean().default(true), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("ntfy"), + serverUrl: z.string().min(1, { message: "Server URL is required" }), + topic: z.string().min(1, { message: "Topic is required" }), + accessToken: z.string().optional(), + priority: z.number().min(1).max(5).default(3), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("pushover"), + userKey: z.string().min(1, { message: "User Key is required" }), + apiToken: z.string().min(1, { message: "API Token is required" }), + priority: z.number().min(-2).max(2).default(0), + retry: z.number().min(30).nullish(), + expire: z.number().min(1).max(10800).nullish(), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("custom"), + endpoint: z.string().min(1, { message: "Endpoint URL is required" }), + headers: z + .array( + z.object({ + key: z.string(), + value: z.string(), + }), + ) + .optional() + .default([]), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("lark"), + webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), + }) + .merge(notificationBaseSchema), +]); + +export const notificationsMap = { + slack: { + icon: , + label: "Slack", + }, + telegram: { + icon: , + label: "Telegram", + }, + discord: { + icon: , + label: "Discord", + }, + lark: { + icon: , + label: "Lark", + }, + email: { + icon: , + label: "Email", + }, + resend: { + icon: , + label: "Resend", + }, + gotify: { + icon: , + label: "Gotify", + }, + ntfy: { + icon: , + label: "ntfy", + }, + pushover: { + icon: , + label: "Pushover", + }, + custom: { + icon: , + label: "Custom", + }, +}; + +export type NotificationSchema = z.infer; + +interface Props { + notificationId?: string; +} + +export const HandleNotifications = ({ notificationId }: Props) => { + const utils = api.useUtils(); + const [visible, setVisible] = useState(false); + const { data: isCloud } = api.settings.isCloud.useQuery(); + + const { data: notification } = api.notification.one.useQuery( + { + notificationId: notificationId || "", + }, + { + enabled: !!notificationId, + }, + ); + const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } = + api.notification.testSlackConnection.useMutation(); + const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } = + api.notification.testTelegramConnection.useMutation(); + const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } = + api.notification.testDiscordConnection.useMutation(); + const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } = + api.notification.testEmailConnection.useMutation(); + const { mutateAsync: testResendConnection, isLoading: isLoadingResend } = + api.notification.testResendConnection.useMutation(); + const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } = + api.notification.testGotifyConnection.useMutation(); + const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } = + api.notification.testNtfyConnection.useMutation(); + const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } = + api.notification.testLarkConnection.useMutation(); + + const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } = + api.notification.testCustomConnection.useMutation(); + + const { mutateAsync: testPushoverConnection, isLoading: isLoadingPushover } = + api.notification.testPushoverConnection.useMutation(); + + const customMutation = notificationId + ? api.notification.updateCustom.useMutation() + : api.notification.createCustom.useMutation(); + const slackMutation = notificationId + ? api.notification.updateSlack.useMutation() + : api.notification.createSlack.useMutation(); + const telegramMutation = notificationId + ? api.notification.updateTelegram.useMutation() + : api.notification.createTelegram.useMutation(); + const discordMutation = notificationId + ? api.notification.updateDiscord.useMutation() + : api.notification.createDiscord.useMutation(); + const emailMutation = notificationId + ? api.notification.updateEmail.useMutation() + : api.notification.createEmail.useMutation(); + const resendMutation = notificationId + ? api.notification.updateResend.useMutation() + : api.notification.createResend.useMutation(); + const gotifyMutation = notificationId + ? api.notification.updateGotify.useMutation() + : api.notification.createGotify.useMutation(); + const ntfyMutation = notificationId + ? api.notification.updateNtfy.useMutation() + : api.notification.createNtfy.useMutation(); + const larkMutation = notificationId + ? api.notification.updateLark.useMutation() + : api.notification.createLark.useMutation(); + const pushoverMutation = notificationId + ? api.notification.updatePushover.useMutation() + : api.notification.createPushover.useMutation(); + + const form = useForm({ + defaultValues: { + type: "slack", + webhookUrl: "", + channel: "", + name: "", + }, + resolver: zodResolver(notificationSchema), + }); + const type = form.watch("type"); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "toAddresses" as never, + }); + + const { + fields: headerFields, + append: appendHeader, + remove: removeHeader, + } = useFieldArray({ + control: form.control, + name: "headers" as never, + }); + + useEffect(() => { + if ((type === "email" || type === "resend") && fields.length === 0) { + append(""); + } + }, [type, append, fields.length]); + + useEffect(() => { + if (notification) { + if (notification.notificationType === "slack") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + dockerCleanup: notification.dockerCleanup, + webhookUrl: notification.slack?.webhookUrl, + channel: notification.slack?.channel || "", + name: notification.name, + type: notification.notificationType, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "telegram") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + botToken: notification.telegram?.botToken, + messageThreadId: notification.telegram?.messageThreadId || "", + chatId: notification.telegram?.chatId, + type: notification.notificationType, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "discord") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + webhookUrl: notification.discord?.webhookUrl, + decoration: notification.discord?.decoration || undefined, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "email") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + smtpServer: notification.email?.smtpServer, + smtpPort: notification.email?.smtpPort, + username: notification.email?.username, + password: notification.email?.password, + toAddresses: notification.email?.toAddresses, + fromAddress: notification.email?.fromAddress, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "resend") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + apiKey: notification.resend?.apiKey, + toAddresses: notification.resend?.toAddresses, + fromAddress: notification.resend?.fromAddress, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "gotify") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + appToken: notification.gotify?.appToken, + decoration: notification.gotify?.decoration || undefined, + priority: notification.gotify?.priority, + serverUrl: notification.gotify?.serverUrl, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + }); + } else if (notification.notificationType === "ntfy") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + accessToken: notification.ntfy?.accessToken || "", + topic: notification.ntfy?.topic, + priority: notification.ntfy?.priority, + serverUrl: notification.ntfy?.serverUrl, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "lark") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + type: notification.notificationType, + webhookUrl: notification.lark?.webhookUrl, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + volumeBackup: notification.volumeBackup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "custom") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + type: notification.notificationType, + endpoint: notification.custom?.endpoint || "", + headers: notification.custom?.headers + ? Object.entries(notification.custom.headers).map( + ([key, value]) => ({ + key, + value, + }), + ) + : [], + name: notification.name, + volumeBackup: notification.volumeBackup, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "pushover") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + userKey: notification.pushover?.userKey, + apiToken: notification.pushover?.apiToken, + priority: notification.pushover?.priority, + retry: notification.pushover?.retry ?? undefined, + expire: notification.pushover?.expire ?? undefined, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } + } else { + form.reset(); + } + }, [form, form.reset, form.formState.isSubmitSuccessful, notification]); + + const activeMutation = { + slack: slackMutation, + telegram: telegramMutation, + discord: discordMutation, + email: emailMutation, + resend: resendMutation, + gotify: gotifyMutation, + ntfy: ntfyMutation, + lark: larkMutation, + custom: customMutation, + pushover: pushoverMutation, + }; + + const onSubmit = async (data: NotificationSchema) => { + const { + appBuildError, + appDeploy, + dokployRestart, + databaseBackup, + volumeBackup, + dockerCleanup, + serverThreshold, + } = data; + let promise: Promise | null = null; + if (data.type === "slack") { + promise = slackMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + webhookUrl: data.webhookUrl, + channel: data.channel, + name: data.name, + dockerCleanup: dockerCleanup, + slackId: notification?.slackId || "", + notificationId: notificationId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "telegram") { + promise = telegramMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + botToken: data.botToken, + messageThreadId: data.messageThreadId || "", + chatId: data.chatId, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + telegramId: notification?.telegramId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "discord") { + promise = discordMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + webhookUrl: data.webhookUrl, + decoration: data.decoration, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + discordId: notification?.discordId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "email") { + promise = emailMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + smtpServer: data.smtpServer, + smtpPort: data.smtpPort, + username: data.username, + password: data.password, + fromAddress: data.fromAddress, + toAddresses: data.toAddresses, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + emailId: notification?.emailId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "resend") { + promise = resendMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + apiKey: data.apiKey, + fromAddress: data.fromAddress, + toAddresses: data.toAddresses, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + resendId: notification?.resendId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "gotify") { + promise = gotifyMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + serverUrl: data.serverUrl, + appToken: data.appToken, + priority: data.priority, + name: data.name, + dockerCleanup: dockerCleanup, + decoration: data.decoration, + notificationId: notificationId || "", + gotifyId: notification?.gotifyId || "", + }); + } else if (data.type === "ntfy") { + promise = ntfyMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + serverUrl: data.serverUrl, + accessToken: data.accessToken || "", + topic: data.topic, + priority: data.priority, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + ntfyId: notification?.ntfyId || "", + }); + } else if (data.type === "lark") { + promise = larkMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + webhookUrl: data.webhookUrl, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + larkId: notification?.larkId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "custom") { + // Convert headers array to object + const headersRecord = + data.headers && data.headers.length > 0 + ? data.headers.reduce( + (acc, { key, value }) => { + if (key.trim()) acc[key] = value; + return acc; + }, + {} as Record, + ) + : undefined; + + promise = customMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + endpoint: data.endpoint, + headers: headersRecord, + name: data.name, + dockerCleanup: dockerCleanup, + serverThreshold: serverThreshold, + notificationId: notificationId || "", + customId: notification?.customId || "", + }); + } else if (data.type === "pushover") { + if (data.priority === 2 && (data.retry == null || data.expire == null)) { + toast.error("Retry and expire are required for emergency priority (2)"); + return; + } + promise = pushoverMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + userKey: data.userKey, + apiToken: data.apiToken, + priority: data.priority, + retry: data.priority === 2 ? data.retry : undefined, + expire: data.priority === 2 ? data.expire : undefined, + name: data.name, + dockerCleanup: dockerCleanup, + serverThreshold: serverThreshold, + notificationId: notificationId || "", + pushoverId: notification?.pushoverId || "", + }); + } + + if (promise) { + await promise + .then(async () => { + toast.success( + notificationId ? "Notification Updated" : "Notification Created", + ); + form.reset({ + type: "slack", + webhookUrl: "", + }); + setVisible(false); + await utils.notification.all.invalidate(); + }) + .catch(() => { + toast.error( + notificationId + ? "Error updating a notification" + : "Error creating a notification", + ); + }); + } + }; + return ( + + + {notificationId ? ( + + ) : ( + + )} + + + + + {notificationId ? "Update" : "Add"} Notification + + + {notificationId + ? "Update your notification providers for multiple channels." + : "Create new notification providers for multiple channels."} + + +
+ + ( + + + Select a provider + + + + {Object.entries(notificationsMap).map(([key, value]) => ( + + +
+ + +
+
+
+ ))} +
+
+ + {activeMutation[field.value].isError && ( +
+ + + {activeMutation[field.value].error?.message} + +
+ )} +
+ )} + /> + +
+ + Fill the next fields. + +
+ ( + + Name + + + + + + + )} + /> + + {type === "slack" && ( + <> + ( + + Webhook URL + + + + + + + )} + /> + + ( + + Channel + + + + + + + )} + /> + + )} + + {type === "telegram" && ( + <> + ( + + Bot Token + + + + + + + )} + /> + + ( + + Chat ID + + + + + + )} + /> + + ( + + Message Thread ID + + + + + + + Optional. Use it when you want to send notifications + to a specific topic in a group. + + + )} + /> + + )} + + {type === "discord" && ( + <> + ( + + Webhook URL + + + + + + + )} + /> + + ( + +
+ Decoration + + Decorate the notification with emojis. + +
+ + + +
+ )} + /> + + )} + + {type === "email" && ( + <> +
+ ( + + SMTP Server + + + + + + + )} + /> + ( + + SMTP Port + + { + const value = e.target.value; + if (value === "") { + field.onChange(undefined); + } else { + const port = Number.parseInt(value); + if (port > 0 && port < 65536) { + field.onChange(port); + } + } + }} + value={field.value || ""} + type="number" + /> + + + + + )} + /> +
+ +
+ ( + + Username + + + + + + + )} + /> + + ( + + Password + + + + + + + )} + /> +
+ + ( + + From Address + + + + + + )} + /> +
+ To Addresses + + {fields.map((field, index) => ( +
+ ( + + + + + + + + )} + /> + +
+ ))} + {type === "email" && + "toAddresses" in form.formState.errors && ( +
+ {form.formState?.errors?.toAddresses?.root?.message} +
+ )} +
+ + + + )} + + {type === "resend" && ( + <> + ( + + API Key + + + + + + )} + /> + + ( + + From Address + + + + + + )} + /> + +
+ To Addresses + + {fields.map((field, index) => ( +
+ ( + + + + + + + + )} + /> + +
+ ))} + {type === "resend" && + "toAddresses" in form.formState.errors && ( +
+ {form.formState?.errors?.toAddresses?.root?.message} +
+ )} +
+ + + + )} + + {type === "gotify" && ( + <> + ( + + Server URL + + + + + + )} + /> + ( + + App Token + + + + + + )} + /> + ( + + Priority + + { + const value = e.target.value; + if (value) { + const port = Number.parseInt(value); + if (port > 0 && port < 10) { + field.onChange(port); + } + } + }} + type="number" + /> + + + Message priority (1-10, default: 5) + + + + )} + /> + ( + +
+ Decoration + + Decorate the notification with emojis. + +
+ + + +
+ )} + /> + + )} + + {type === "ntfy" && ( + <> + ( + + Server URL + + + + + + )} + /> + ( + + Topic + + + + + + )} + /> + ( + + Access Token + + + + + Optional. Leave blank for public topics. + + + + )} + /> + ( + + Priority + + { + const value = e.target.value; + if (value) { + const port = Number.parseInt(value); + if (port > 0 && port <= 5) { + field.onChange(port); + } + } + }} + type="number" + /> + + + Message priority (1-5, default: 3) + + + + )} + /> + + )} + {type === "custom" && ( +
+ ( + + Webhook URL + + + + + The URL where POST requests will be sent with + notification data. + + + + )} + /> + +
+
+ Headers + + Optional. Custom headers for your POST request (e.g., + Authorization, Content-Type). + +
+ +
+ {headerFields.map((field, index) => ( +
+ ( + + + + + + )} + /> + ( + + + + + + )} + /> + +
+ ))} +
+ + +
+
+ )} + {type === "lark" && ( + <> + ( + + Webhook URL + + + + + + )} + /> + + )} + {type === "pushover" && ( + <> + ( + + User Key + + + + + + )} + /> + ( + + API Token + + + + + + )} + /> + ( + + Priority + + { + const value = e.target.value; + if (value === "" || value === "-") { + field.onChange(0); + } else { + const priority = Number.parseInt(value); + if ( + !Number.isNaN(priority) && + priority >= -2 && + priority <= 2 + ) { + field.onChange(priority); + } + } + }} + type="number" + min={-2} + max={2} + /> + + + Message priority (-2 to 2, default: 0, emergency: 2) + + + + )} + /> + {form.watch("priority") === 2 && ( + <> + ( + + Retry (seconds) + + { + const value = e.target.value; + if (value === "") { + field.onChange(undefined); + } else { + const retry = Number.parseInt(value); + if (!Number.isNaN(retry)) { + field.onChange(retry); + } + } + }} + type="number" + min={30} + /> + + + How often (in seconds) to retry. Minimum 30 + seconds. + + + + )} + /> + ( + + Expire (seconds) + + { + const value = e.target.value; + if (value === "") { + field.onChange(undefined); + } else { + const expire = Number.parseInt(value); + if (!Number.isNaN(expire)) { + field.onChange(expire); + } + } + }} + type="number" + min={1} + max={10800} + /> + + + How long to keep retrying (max 10800 seconds / 3 + hours). + + + + )} + /> + + )} + + )} +
+
+
+ + Select the actions. + + +
+ ( + +
+ App Deploy + + Trigger the action when a app is deployed. + +
+ + + +
+ )} + /> + ( + +
+ App Build Error + + Trigger the action when the build fails. + +
+ + + +
+ )} + /> + + ( + +
+ Database Backup + + Trigger the action when a database backup is created. + +
+ + + +
+ )} + /> + + ( + +
+ Volume Backup + + Trigger the action when a volume backup is created. + +
+ + + +
+ )} + /> + + ( + +
+ Docker Cleanup + + Trigger the action when the docker cleanup is + performed. + +
+ + + +
+ )} + /> + + {!isCloud && ( + ( + +
+ Dokploy Restart + + Trigger the action when dokploy is restarted. + +
+ + + +
+ )} + /> + )} + + {isCloud && ( + ( + +
+ Server Threshold + + Trigger the action when the server threshold is + reached. + +
+ + + +
+ )} + /> + )} +
+
+ + + + + + + +
+
+ ); +}; diff --git a/apps/dokploy/components/icons/notification-icons.tsx b/apps/dokploy/components/icons/notification-icons.tsx index 10bccbcd4..05f66146a 100644 --- a/apps/dokploy/components/icons/notification-icons.tsx +++ b/apps/dokploy/components/icons/notification-icons.tsx @@ -1,279 +1,279 @@ -import { cn } from "@/lib/utils"; - -interface Props { - className?: string; -} -export const SlackIcon = ({ className }: Props) => { - return ( - - - - - - - - - ); -}; - -export const TelegramIcon = ({ className }: Props) => { - return ( - - - - - - - - - - - ); -}; - -export const DiscordIcon = ({ className }: Props) => { - return ( - - - - ); -}; -export const LarkIcon = ({ className }: Props) => { - return ( - - - - - - ); -}; -export const GotifyIcon = ({ className }: Props) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export const NtfyIcon = ({ className }: Props) => { - return ( - - - - ); -}; - -export const PushoverIcon = ({ className }: Props) => { - return ( - - - - - - - ); -}; - -export const ResendIcon = ({ className }: Props) => { - return ( - - - - - ); -}; +import { cn } from "@/lib/utils"; + +interface Props { + className?: string; +} +export const SlackIcon = ({ className }: Props) => { + return ( + + + + + + + + + ); +}; + +export const TelegramIcon = ({ className }: Props) => { + return ( + + + + + + + + + + + ); +}; + +export const DiscordIcon = ({ className }: Props) => { + return ( + + + + ); +}; +export const LarkIcon = ({ className }: Props) => { + return ( + + + + + + ); +}; +export const GotifyIcon = ({ className }: Props) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const NtfyIcon = ({ className }: Props) => { + return ( + + + + ); +}; + +export const PushoverIcon = ({ className }: Props) => { + return ( + + + + + + + ); +}; + +export const ResendIcon = ({ className }: Props) => { + return ( + + + + + ); +}; diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts index a5097288b..97361b5b4 100644 --- a/apps/dokploy/server/api/routers/notification.ts +++ b/apps/dokploy/server/api/routers/notification.ts @@ -1,4 +1,4 @@ -import { +import { createCustomNotification, createDiscordNotification, createEmailNotification, @@ -12,7 +12,7 @@ import { findNotificationById, getWebServerSettings, IS_CLOUD, - removeNotificationById, + removeNotificationById, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -35,18 +35,18 @@ import { updateSlackNotification, updateTelegramNotification, } from "@dokploy/server"; -import { TRPCError } from "@trpc/server"; -import { desc, eq, sql } from "drizzle-orm"; -import { z } from "zod"; -import { - adminProcedure, - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "@/server/api/trpc"; -import { db } from "@/server/db"; -import { - apiCreateCustom, +import { TRPCError } from "@trpc/server"; +import { desc, eq, sql } from "drizzle-orm"; +import { z } from "zod"; +import { + adminProcedure, + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { + apiCreateCustom, apiCreateDiscord, apiCreateEmail, apiCreateGotify, @@ -79,695 +79,695 @@ import { apiUpdateTelegram, notifications, server, -} from "@/server/db/schema"; - -export const notificationRouter = createTRPCRouter({ - createSlack: adminProcedure - .input(apiCreateSlack) - .mutation(async ({ input, ctx }) => { - try { - return await createSlackNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateSlack: adminProcedure - .input(apiUpdateSlack) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateSlackNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testSlackConnection: adminProcedure - .input(apiTestSlackConnection) - .mutation(async ({ input }) => { - try { - await sendSlackNotification(input, { - channel: input.channel, - text: "Hi, From Dokploy 👋", - }); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `${error instanceof Error ? error.message : "Unknown error"}`, - cause: error, - }); - } - }), - createTelegram: adminProcedure - .input(apiCreateTelegram) - .mutation(async ({ input, ctx }) => { - try { - return await createTelegramNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - - updateTelegram: adminProcedure - .input(apiUpdateTelegram) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateTelegramNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error updating the notification", - cause: error, - }); - } - }), - testTelegramConnection: adminProcedure - .input(apiTestTelegramConnection) - .mutation(async ({ input }) => { - try { - await sendTelegramNotification(input, "Hi, From Dokploy 👋"); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error testing the notification", - cause: error, - }); - } - }), - createDiscord: adminProcedure - .input(apiCreateDiscord) - .mutation(async ({ input, ctx }) => { - try { - return await createDiscordNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - - updateDiscord: adminProcedure - .input(apiUpdateDiscord) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateDiscordNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error updating the notification", - cause: error, - }); - } - }), - - testDiscordConnection: adminProcedure - .input(apiTestDiscordConnection) - .mutation(async ({ input }) => { - try { - const decorate = (decoration: string, text: string) => - `${input.decoration ? decoration : ""} ${text}`.trim(); - - await sendDiscordNotification(input, { - title: decorate(">", "`🤚` - Test Notification"), - description: decorate(">", "Hi, From Dokploy 👋"), - color: 0xf3f7f4, - }); - - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `${error instanceof Error ? error.message : "Unknown error"}`, - cause: error, - }); - } - }), - createEmail: adminProcedure - .input(apiCreateEmail) - .mutation(async ({ input, ctx }) => { - try { - return await createEmailNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateEmail: adminProcedure - .input(apiUpdateEmail) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateEmailNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error updating the notification", - cause: error, - }); - } - }), - testEmailConnection: adminProcedure - .input(apiTestEmailConnection) - .mutation(async ({ input }) => { - try { - await sendEmailNotification( - input, - "Test Email", - "

Hi, From Dokploy 👋

", - ); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `${error instanceof Error ? error.message : "Unknown error"}`, - cause: error, - }); - } - }), - createResend: adminProcedure - .input(apiCreateResend) - .mutation(async ({ input, ctx }) => { - try { - return await createResendNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateResend: adminProcedure - .input(apiUpdateResend) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateResendNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error updating the notification", - cause: error, - }); - } - }), - testResendConnection: adminProcedure - .input(apiTestResendConnection) - .mutation(async ({ input }) => { - try { - await sendResendNotification( - input, - "Test Email", - "

Hi, From Dokploy 👋

", - ); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `${error instanceof Error ? error.message : "Unknown error"}`, - cause: error, - }); - } - }), - remove: adminProcedure - .input(apiFindOneNotification) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to delete this notification", - }); - } - return await removeNotificationById(input.notificationId); - } catch (error) { - const message = - error instanceof Error - ? error.message - : "Error deleting this notification"; - throw new TRPCError({ - code: "BAD_REQUEST", - message, - }); - } - }), - one: protectedProcedure - .input(apiFindOneNotification) - .query(async ({ input, ctx }) => { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this notification", - }); - } - return notification; - }), - all: adminProcedure.query(async ({ ctx }) => { - return await db.query.notifications.findMany({ - with: { - slack: true, - telegram: true, - discord: true, - email: true, - resend: true, - gotify: true, - ntfy: true, - custom: true, - lark: true, - pushover: true, - }, - orderBy: desc(notifications.createdAt), - where: eq(notifications.organizationId, ctx.session.activeOrganizationId), - }); - }), - receiveNotification: publicProcedure - .input( - z.object({ - ServerType: z.enum(["Dokploy", "Remote"]).default("Dokploy"), - Type: z.enum(["Memory", "CPU"]), - Value: z.number(), - Threshold: z.number(), - Message: z.string(), - Timestamp: z.string(), - Token: z.string(), - }), - ) - .mutation(async ({ input }) => { - try { - let organizationId = ""; - let ServerName = ""; - if (input.ServerType === "Dokploy") { - const settings = await getWebServerSettings(); - if ( - !settings?.metricsConfig?.server?.token || - settings.metricsConfig.server.token !== input.Token - ) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Token not found", - }); - } - - // For Dokploy server type, we don't have a specific organizationId - // This might need to be adjusted based on your business logic - organizationId = ""; - ServerName = "Dokploy"; - } else { - const result = await db - .select() - .from(server) - .where( - sql`${server.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`, - ); - - if (!result?.[0]?.organizationId) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Token not found", - }); - } - - organizationId = result?.[0]?.organizationId; - ServerName = "Remote"; - } - - await sendServerThresholdNotifications(organizationId, { - ...input, - ServerName, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error sending the notification", - cause: error, - }); - } - }), - createGotify: adminProcedure - .input(apiCreateGotify) - .mutation(async ({ input, ctx }) => { - try { - return await createGotifyNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateGotify: adminProcedure - .input(apiUpdateGotify) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if ( - IS_CLOUD && - notification.organizationId !== ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateGotifyNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testGotifyConnection: adminProcedure - .input(apiTestGotifyConnection) - .mutation(async ({ input }) => { - try { - await sendGotifyNotification( - input, - "Test Notification", - "Hi, From Dokploy 👋", - ); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error testing the notification", - cause: error, - }); - } - }), - createNtfy: adminProcedure - .input(apiCreateNtfy) - .mutation(async ({ input, ctx }) => { - try { - return await createNtfyNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateNtfy: adminProcedure - .input(apiUpdateNtfy) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if ( - IS_CLOUD && - notification.organizationId !== ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateNtfyNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testNtfyConnection: adminProcedure - .input(apiTestNtfyConnection) - .mutation(async ({ input }) => { - try { - await sendNtfyNotification( - input, - "Test Notification", - "", - "view, visit Dokploy on Github, https://github.com/dokploy/dokploy, clear=true;", - "Hi, From Dokploy 👋", - ); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error testing the notification", - cause: error, - }); - } - }), - createCustom: adminProcedure - .input(apiCreateCustom) - .mutation(async ({ input, ctx }) => { - try { - return await createCustomNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateCustom: adminProcedure - .input(apiUpdateCustom) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateCustomNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testCustomConnection: adminProcedure - .input(apiTestCustomConnection) - .mutation(async ({ input }) => { - try { - await sendCustomNotification(input, { - title: "Test Notification", - message: "Hi, From Dokploy 👋", - timestamp: new Date().toISOString(), - }); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `${error instanceof Error ? error.message : "Unknown error"}`, - cause: error, - }); - } - }), - createLark: adminProcedure - .input(apiCreateLark) - .mutation(async ({ input, ctx }) => { - try { - return await createLarkNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateLark: adminProcedure - .input(apiUpdateLark) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if ( - IS_CLOUD && - notification.organizationId !== ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateLarkNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testLarkConnection: adminProcedure - .input(apiTestLarkConnection) - .mutation(async ({ input }) => { - try { - await sendLarkNotification(input, { - msg_type: "text", - content: { - text: "Hi, From Dokploy 👋", - }, - }); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error testing the notification", - cause: error, - }); - } - }), - createPushover: adminProcedure - .input(apiCreatePushover) - .mutation(async ({ input, ctx }) => { - try { - return await createPushoverNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updatePushover: adminProcedure - .input(apiUpdatePushover) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if ( - IS_CLOUD && - notification.organizationId !== ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updatePushoverNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testPushoverConnection: adminProcedure - .input(apiTestPushoverConnection) - .mutation(async ({ input }) => { - try { - await sendPushoverNotification( - input, - "Test Notification", - "Hi, From Dokploy 👋", - ); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error testing the notification", - cause: error, - }); - } - }), - getEmailProviders: adminProcedure.query(async ({ ctx }) => { - return await db.query.notifications.findMany({ - where: eq(notifications.organizationId, ctx.session.activeOrganizationId), - with: { - email: true, - resend: true, - }, - }); - }), -}); +} from "@/server/db/schema"; + +export const notificationRouter = createTRPCRouter({ + createSlack: adminProcedure + .input(apiCreateSlack) + .mutation(async ({ input, ctx }) => { + try { + return await createSlackNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateSlack: adminProcedure + .input(apiUpdateSlack) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateSlackNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testSlackConnection: adminProcedure + .input(apiTestSlackConnection) + .mutation(async ({ input }) => { + try { + await sendSlackNotification(input, { + channel: input.channel, + text: "Hi, From Dokploy 👋", + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `${error instanceof Error ? error.message : "Unknown error"}`, + cause: error, + }); + } + }), + createTelegram: adminProcedure + .input(apiCreateTelegram) + .mutation(async ({ input, ctx }) => { + try { + return await createTelegramNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + + updateTelegram: adminProcedure + .input(apiUpdateTelegram) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateTelegramNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error updating the notification", + cause: error, + }); + } + }), + testTelegramConnection: adminProcedure + .input(apiTestTelegramConnection) + .mutation(async ({ input }) => { + try { + await sendTelegramNotification(input, "Hi, From Dokploy 👋"); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), + createDiscord: adminProcedure + .input(apiCreateDiscord) + .mutation(async ({ input, ctx }) => { + try { + return await createDiscordNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + + updateDiscord: adminProcedure + .input(apiUpdateDiscord) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateDiscordNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error updating the notification", + cause: error, + }); + } + }), + + testDiscordConnection: adminProcedure + .input(apiTestDiscordConnection) + .mutation(async ({ input }) => { + try { + const decorate = (decoration: string, text: string) => + `${input.decoration ? decoration : ""} ${text}`.trim(); + + await sendDiscordNotification(input, { + title: decorate(">", "`🤚` - Test Notification"), + description: decorate(">", "Hi, From Dokploy 👋"), + color: 0xf3f7f4, + }); + + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `${error instanceof Error ? error.message : "Unknown error"}`, + cause: error, + }); + } + }), + createEmail: adminProcedure + .input(apiCreateEmail) + .mutation(async ({ input, ctx }) => { + try { + return await createEmailNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateEmail: adminProcedure + .input(apiUpdateEmail) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateEmailNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error updating the notification", + cause: error, + }); + } + }), + testEmailConnection: adminProcedure + .input(apiTestEmailConnection) + .mutation(async ({ input }) => { + try { + await sendEmailNotification( + input, + "Test Email", + "

Hi, From Dokploy 👋

", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `${error instanceof Error ? error.message : "Unknown error"}`, + cause: error, + }); + } + }), + createResend: adminProcedure + .input(apiCreateResend) + .mutation(async ({ input, ctx }) => { + try { + return await createResendNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateResend: adminProcedure + .input(apiUpdateResend) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateResendNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error updating the notification", + cause: error, + }); + } + }), + testResendConnection: adminProcedure + .input(apiTestResendConnection) + .mutation(async ({ input }) => { + try { + await sendResendNotification( + input, + "Test Email", + "

Hi, From Dokploy 👋

", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `${error instanceof Error ? error.message : "Unknown error"}`, + cause: error, + }); + } + }), + remove: adminProcedure + .input(apiFindOneNotification) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this notification", + }); + } + return await removeNotificationById(input.notificationId); + } catch (error) { + const message = + error instanceof Error + ? error.message + : "Error deleting this notification"; + throw new TRPCError({ + code: "BAD_REQUEST", + message, + }); + } + }), + one: protectedProcedure + .input(apiFindOneNotification) + .query(async ({ input, ctx }) => { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this notification", + }); + } + return notification; + }), + all: adminProcedure.query(async ({ ctx }) => { + return await db.query.notifications.findMany({ + with: { + slack: true, + telegram: true, + discord: true, + email: true, + resend: true, + gotify: true, + ntfy: true, + custom: true, + lark: true, + pushover: true, + }, + orderBy: desc(notifications.createdAt), + where: eq(notifications.organizationId, ctx.session.activeOrganizationId), + }); + }), + receiveNotification: publicProcedure + .input( + z.object({ + ServerType: z.enum(["Dokploy", "Remote"]).default("Dokploy"), + Type: z.enum(["Memory", "CPU"]), + Value: z.number(), + Threshold: z.number(), + Message: z.string(), + Timestamp: z.string(), + Token: z.string(), + }), + ) + .mutation(async ({ input }) => { + try { + let organizationId = ""; + let ServerName = ""; + if (input.ServerType === "Dokploy") { + const settings = await getWebServerSettings(); + if ( + !settings?.metricsConfig?.server?.token || + settings.metricsConfig.server.token !== input.Token + ) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Token not found", + }); + } + + // For Dokploy server type, we don't have a specific organizationId + // This might need to be adjusted based on your business logic + organizationId = ""; + ServerName = "Dokploy"; + } else { + const result = await db + .select() + .from(server) + .where( + sql`${server.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`, + ); + + if (!result?.[0]?.organizationId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Token not found", + }); + } + + organizationId = result?.[0]?.organizationId; + ServerName = "Remote"; + } + + await sendServerThresholdNotifications(organizationId, { + ...input, + ServerName, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error sending the notification", + cause: error, + }); + } + }), + createGotify: adminProcedure + .input(apiCreateGotify) + .mutation(async ({ input, ctx }) => { + try { + return await createGotifyNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateGotify: adminProcedure + .input(apiUpdateGotify) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if ( + IS_CLOUD && + notification.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateGotifyNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testGotifyConnection: adminProcedure + .input(apiTestGotifyConnection) + .mutation(async ({ input }) => { + try { + await sendGotifyNotification( + input, + "Test Notification", + "Hi, From Dokploy 👋", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), + createNtfy: adminProcedure + .input(apiCreateNtfy) + .mutation(async ({ input, ctx }) => { + try { + return await createNtfyNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateNtfy: adminProcedure + .input(apiUpdateNtfy) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if ( + IS_CLOUD && + notification.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateNtfyNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testNtfyConnection: adminProcedure + .input(apiTestNtfyConnection) + .mutation(async ({ input }) => { + try { + await sendNtfyNotification( + input, + "Test Notification", + "", + "view, visit Dokploy on Github, https://github.com/dokploy/dokploy, clear=true;", + "Hi, From Dokploy 👋", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), + createCustom: adminProcedure + .input(apiCreateCustom) + .mutation(async ({ input, ctx }) => { + try { + return await createCustomNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateCustom: adminProcedure + .input(apiUpdateCustom) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateCustomNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testCustomConnection: adminProcedure + .input(apiTestCustomConnection) + .mutation(async ({ input }) => { + try { + await sendCustomNotification(input, { + title: "Test Notification", + message: "Hi, From Dokploy 👋", + timestamp: new Date().toISOString(), + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `${error instanceof Error ? error.message : "Unknown error"}`, + cause: error, + }); + } + }), + createLark: adminProcedure + .input(apiCreateLark) + .mutation(async ({ input, ctx }) => { + try { + return await createLarkNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateLark: adminProcedure + .input(apiUpdateLark) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if ( + IS_CLOUD && + notification.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateLarkNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testLarkConnection: adminProcedure + .input(apiTestLarkConnection) + .mutation(async ({ input }) => { + try { + await sendLarkNotification(input, { + msg_type: "text", + content: { + text: "Hi, From Dokploy 👋", + }, + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), + createPushover: adminProcedure + .input(apiCreatePushover) + .mutation(async ({ input, ctx }) => { + try { + return await createPushoverNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updatePushover: adminProcedure + .input(apiUpdatePushover) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if ( + IS_CLOUD && + notification.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updatePushoverNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testPushoverConnection: adminProcedure + .input(apiTestPushoverConnection) + .mutation(async ({ input }) => { + try { + await sendPushoverNotification( + input, + "Test Notification", + "Hi, From Dokploy 👋", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), + getEmailProviders: adminProcedure.query(async ({ ctx }) => { + return await db.query.notifications.findMany({ + where: eq(notifications.organizationId, ctx.session.activeOrganizationId), + with: { + email: true, + resend: true, + }, + }); + }), +}); diff --git a/packages/server/src/services/notification.ts b/packages/server/src/services/notification.ts index f11619aac..ee329c457 100644 --- a/packages/server/src/services/notification.ts +++ b/packages/server/src/services/notification.ts @@ -1,8 +1,8 @@ -import { db } from "@dokploy/server/db"; -import { - type apiCreateCustom, - type apiCreateDiscord, - type apiCreateEmail, +import { db } from "@dokploy/server/db"; +import { + type apiCreateCustom, + type apiCreateDiscord, + type apiCreateEmail, type apiCreateGotify, type apiCreateLark, type apiCreateNtfy, @@ -32,986 +32,986 @@ import { slack, telegram, } from "@dokploy/server/db/schema"; -import { TRPCError } from "@trpc/server"; -import { eq } from "drizzle-orm"; - -export type Notification = typeof notifications.$inferSelect; - -export const createSlackNotification = async ( - input: typeof apiCreateSlack._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newSlack = await tx - .insert(slack) - .values({ - channel: input.channel, - webhookUrl: input.webhookUrl, - }) - .returning() - .then((value) => value[0]); - - if (!newSlack) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting slack", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - slackId: newSlack.slackId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "slack", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateSlackNotification = async ( - input: typeof apiUpdateSlack._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(slack) - .set({ - channel: input.channel, - webhookUrl: input.webhookUrl, - }) - .where(eq(slack.slackId, input.slackId)) - .returning() - .then((value) => value[0]); - - return newDestination; - }); -}; - -export const createTelegramNotification = async ( - input: typeof apiCreateTelegram._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newTelegram = await tx - .insert(telegram) - .values({ - botToken: input.botToken, - chatId: input.chatId, - messageThreadId: input.messageThreadId, - }) - .returning() - .then((value) => value[0]); - - if (!newTelegram) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting telegram", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - telegramId: newTelegram.telegramId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "telegram", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateTelegramNotification = async ( - input: typeof apiUpdateTelegram._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(telegram) - .set({ - botToken: input.botToken, - chatId: input.chatId, - messageThreadId: input.messageThreadId, - }) - .where(eq(telegram.telegramId, input.telegramId)) - .returning() - .then((value) => value[0]); - - return newDestination; - }); -}; - -export const createDiscordNotification = async ( - input: typeof apiCreateDiscord._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newDiscord = await tx - .insert(discord) - .values({ - webhookUrl: input.webhookUrl, - decoration: input.decoration, - }) - .returning() - .then((value) => value[0]); - - if (!newDiscord) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting discord", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - discordId: newDiscord.discordId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "discord", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateDiscordNotification = async ( - input: typeof apiUpdateDiscord._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(discord) - .set({ - webhookUrl: input.webhookUrl, - decoration: input.decoration, - }) - .where(eq(discord.discordId, input.discordId)) - .returning() - .then((value) => value[0]); - - return newDestination; - }); -}; - -export const createEmailNotification = async ( - input: typeof apiCreateEmail._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newEmail = await tx - .insert(email) - .values({ - smtpServer: input.smtpServer, - smtpPort: input.smtpPort, - username: input.username, - password: input.password, - fromAddress: input.fromAddress, - toAddresses: input.toAddresses, - }) - .returning() - .then((value) => value[0]); - - if (!newEmail) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting email", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - emailId: newEmail.emailId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "email", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateEmailNotification = async ( - input: typeof apiUpdateEmail._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(email) - .set({ - smtpServer: input.smtpServer, - smtpPort: input.smtpPort, - username: input.username, - password: input.password, - fromAddress: input.fromAddress, - toAddresses: input.toAddresses, - }) - .where(eq(email.emailId, input.emailId)) - .returning() - .then((value) => value[0]); - - return newDestination; - }); -}; - -export const createResendNotification = async ( - input: typeof apiCreateResend._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newResend = await tx - .insert(resend) - .values({ - apiKey: input.apiKey, - fromAddress: input.fromAddress, - toAddresses: input.toAddresses, - }) - .returning() - .then((value) => value[0]); - - if (!newResend) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting resend", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - resendId: newResend.resendId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "resend", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateResendNotification = async ( - input: typeof apiUpdateResend._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(resend) - .set({ - apiKey: input.apiKey, - fromAddress: input.fromAddress, - toAddresses: input.toAddresses, - }) - .where(eq(resend.resendId, input.resendId)) - .returning() - .then((value) => value[0]); - - return newDestination; - }); -}; - -export const createGotifyNotification = async ( - input: typeof apiCreateGotify._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newGotify = await tx - .insert(gotify) - .values({ - serverUrl: input.serverUrl, - appToken: input.appToken, - priority: input.priority, - decoration: input.decoration, - }) - .returning() - .then((value) => value[0]); - - if (!newGotify) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting gotify", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - gotifyId: newGotify.gotifyId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "gotify", - organizationId: organizationId, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateGotifyNotification = async ( - input: typeof apiUpdateGotify._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(gotify) - .set({ - serverUrl: input.serverUrl, - appToken: input.appToken, - priority: input.priority, - decoration: input.decoration, - }) - .where(eq(gotify.gotifyId, input.gotifyId)); - - return newDestination; - }); -}; - -export const createNtfyNotification = async ( - input: typeof apiCreateNtfy._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newNtfy = await tx - .insert(ntfy) - .values({ - serverUrl: input.serverUrl, - topic: input.topic, - accessToken: input.accessToken ?? null, - priority: input.priority, - }) - .returning() - .then((value) => value[0]); - - if (!newNtfy) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting ntfy", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - ntfyId: newNtfy.ntfyId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "ntfy", - organizationId: organizationId, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateNtfyNotification = async ( - input: typeof apiUpdateNtfy._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(ntfy) - .set({ - serverUrl: input.serverUrl, - topic: input.topic, - accessToken: input.accessToken ?? null, - priority: input.priority, - }) - .where(eq(ntfy.ntfyId, input.ntfyId)); - - return newDestination; - }); -}; - -export const createCustomNotification = async ( - input: typeof apiCreateCustom._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newCustom = await tx - .insert(custom) - .values({ - endpoint: input.endpoint, - headers: input.headers, - }) - .returning() - .then((value) => value[0]); - - if (!newCustom) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting custom", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - customId: newCustom.customId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "custom", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateCustomNotification = async ( - input: typeof apiUpdateCustom._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(custom) - .set({ - endpoint: input.endpoint, - headers: input.headers, - }) - .where(eq(custom.customId, input.customId)); - - return newDestination; - }); -}; - -export const findNotificationById = async (notificationId: string) => { - const notification = await db.query.notifications.findFirst({ - where: eq(notifications.notificationId, notificationId), - with: { - slack: true, - telegram: true, - discord: true, - email: true, - resend: true, - gotify: true, - ntfy: true, - custom: true, - lark: true, - pushover: true, - }, - }); - if (!notification) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Notification not found", - }); - } - return notification; -}; - -export const removeNotificationById = async (notificationId: string) => { - const result = await db - .delete(notifications) - .where(eq(notifications.notificationId, notificationId)) - .returning(); - - return result[0]; -}; - -export const createLarkNotification = async ( - input: typeof apiCreateLark._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newLark = await tx - .insert(lark) - .values({ - webhookUrl: input.webhookUrl, - }) - .returning() - .then((value) => value[0]); - - if (!newLark) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting lark", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - larkId: newLark.larkId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "lark", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateLarkNotification = async ( - input: typeof apiUpdateLark._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(lark) - .set({ - webhookUrl: input.webhookUrl, - }) - .where(eq(lark.larkId, input.larkId)) - .returning() - .then((value) => value[0]); - - return newDestination; - }); -}; - -export const updateNotificationById = async ( - notificationId: string, - notificationData: Partial, -) => { - const result = await db - .update(notifications) - .set({ - ...notificationData, - }) - .where(eq(notifications.notificationId, notificationId)) - .returning(); - - return result[0]; -}; - -export const createPushoverNotification = async ( - input: typeof apiCreatePushover._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newPushover = await tx - .insert(pushover) - .values({ - userKey: input.userKey, - apiToken: input.apiToken, - priority: input.priority, - retry: input.retry, - expire: input.expire, - }) - .returning() - .then((value) => value[0]); - - if (!newPushover) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting pushover", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - pushoverId: newPushover.pushoverId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - serverThreshold: input.serverThreshold, - notificationType: "pushover", - organizationId: organizationId, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updatePushoverNotification = async ( - input: typeof apiUpdatePushover._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(pushover) - .set({ - userKey: input.userKey, - apiToken: input.apiToken, - priority: input.priority, - retry: input.retry, - expire: input.expire, - }) - .where(eq(pushover.pushoverId, input.pushoverId)); - - return newDestination; - }); -}; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type Notification = typeof notifications.$inferSelect; + +export const createSlackNotification = async ( + input: typeof apiCreateSlack._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newSlack = await tx + .insert(slack) + .values({ + channel: input.channel, + webhookUrl: input.webhookUrl, + }) + .returning() + .then((value) => value[0]); + + if (!newSlack) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting slack", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + slackId: newSlack.slackId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "slack", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateSlackNotification = async ( + input: typeof apiUpdateSlack._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(slack) + .set({ + channel: input.channel, + webhookUrl: input.webhookUrl, + }) + .where(eq(slack.slackId, input.slackId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createTelegramNotification = async ( + input: typeof apiCreateTelegram._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newTelegram = await tx + .insert(telegram) + .values({ + botToken: input.botToken, + chatId: input.chatId, + messageThreadId: input.messageThreadId, + }) + .returning() + .then((value) => value[0]); + + if (!newTelegram) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting telegram", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + telegramId: newTelegram.telegramId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "telegram", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateTelegramNotification = async ( + input: typeof apiUpdateTelegram._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(telegram) + .set({ + botToken: input.botToken, + chatId: input.chatId, + messageThreadId: input.messageThreadId, + }) + .where(eq(telegram.telegramId, input.telegramId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createDiscordNotification = async ( + input: typeof apiCreateDiscord._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newDiscord = await tx + .insert(discord) + .values({ + webhookUrl: input.webhookUrl, + decoration: input.decoration, + }) + .returning() + .then((value) => value[0]); + + if (!newDiscord) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting discord", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + discordId: newDiscord.discordId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "discord", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateDiscordNotification = async ( + input: typeof apiUpdateDiscord._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(discord) + .set({ + webhookUrl: input.webhookUrl, + decoration: input.decoration, + }) + .where(eq(discord.discordId, input.discordId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createEmailNotification = async ( + input: typeof apiCreateEmail._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newEmail = await tx + .insert(email) + .values({ + smtpServer: input.smtpServer, + smtpPort: input.smtpPort, + username: input.username, + password: input.password, + fromAddress: input.fromAddress, + toAddresses: input.toAddresses, + }) + .returning() + .then((value) => value[0]); + + if (!newEmail) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting email", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + emailId: newEmail.emailId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "email", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateEmailNotification = async ( + input: typeof apiUpdateEmail._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(email) + .set({ + smtpServer: input.smtpServer, + smtpPort: input.smtpPort, + username: input.username, + password: input.password, + fromAddress: input.fromAddress, + toAddresses: input.toAddresses, + }) + .where(eq(email.emailId, input.emailId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createResendNotification = async ( + input: typeof apiCreateResend._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newResend = await tx + .insert(resend) + .values({ + apiKey: input.apiKey, + fromAddress: input.fromAddress, + toAddresses: input.toAddresses, + }) + .returning() + .then((value) => value[0]); + + if (!newResend) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting resend", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + resendId: newResend.resendId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "resend", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateResendNotification = async ( + input: typeof apiUpdateResend._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(resend) + .set({ + apiKey: input.apiKey, + fromAddress: input.fromAddress, + toAddresses: input.toAddresses, + }) + .where(eq(resend.resendId, input.resendId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createGotifyNotification = async ( + input: typeof apiCreateGotify._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newGotify = await tx + .insert(gotify) + .values({ + serverUrl: input.serverUrl, + appToken: input.appToken, + priority: input.priority, + decoration: input.decoration, + }) + .returning() + .then((value) => value[0]); + + if (!newGotify) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting gotify", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + gotifyId: newGotify.gotifyId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "gotify", + organizationId: organizationId, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateGotifyNotification = async ( + input: typeof apiUpdateGotify._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(gotify) + .set({ + serverUrl: input.serverUrl, + appToken: input.appToken, + priority: input.priority, + decoration: input.decoration, + }) + .where(eq(gotify.gotifyId, input.gotifyId)); + + return newDestination; + }); +}; + +export const createNtfyNotification = async ( + input: typeof apiCreateNtfy._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newNtfy = await tx + .insert(ntfy) + .values({ + serverUrl: input.serverUrl, + topic: input.topic, + accessToken: input.accessToken ?? null, + priority: input.priority, + }) + .returning() + .then((value) => value[0]); + + if (!newNtfy) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting ntfy", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + ntfyId: newNtfy.ntfyId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "ntfy", + organizationId: organizationId, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateNtfyNotification = async ( + input: typeof apiUpdateNtfy._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(ntfy) + .set({ + serverUrl: input.serverUrl, + topic: input.topic, + accessToken: input.accessToken ?? null, + priority: input.priority, + }) + .where(eq(ntfy.ntfyId, input.ntfyId)); + + return newDestination; + }); +}; + +export const createCustomNotification = async ( + input: typeof apiCreateCustom._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newCustom = await tx + .insert(custom) + .values({ + endpoint: input.endpoint, + headers: input.headers, + }) + .returning() + .then((value) => value[0]); + + if (!newCustom) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting custom", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + customId: newCustom.customId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "custom", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateCustomNotification = async ( + input: typeof apiUpdateCustom._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(custom) + .set({ + endpoint: input.endpoint, + headers: input.headers, + }) + .where(eq(custom.customId, input.customId)); + + return newDestination; + }); +}; + +export const findNotificationById = async (notificationId: string) => { + const notification = await db.query.notifications.findFirst({ + where: eq(notifications.notificationId, notificationId), + with: { + slack: true, + telegram: true, + discord: true, + email: true, + resend: true, + gotify: true, + ntfy: true, + custom: true, + lark: true, + pushover: true, + }, + }); + if (!notification) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Notification not found", + }); + } + return notification; +}; + +export const removeNotificationById = async (notificationId: string) => { + const result = await db + .delete(notifications) + .where(eq(notifications.notificationId, notificationId)) + .returning(); + + return result[0]; +}; + +export const createLarkNotification = async ( + input: typeof apiCreateLark._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newLark = await tx + .insert(lark) + .values({ + webhookUrl: input.webhookUrl, + }) + .returning() + .then((value) => value[0]); + + if (!newLark) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting lark", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + larkId: newLark.larkId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "lark", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateLarkNotification = async ( + input: typeof apiUpdateLark._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(lark) + .set({ + webhookUrl: input.webhookUrl, + }) + .where(eq(lark.larkId, input.larkId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const updateNotificationById = async ( + notificationId: string, + notificationData: Partial, +) => { + const result = await db + .update(notifications) + .set({ + ...notificationData, + }) + .where(eq(notifications.notificationId, notificationId)) + .returning(); + + return result[0]; +}; + +export const createPushoverNotification = async ( + input: typeof apiCreatePushover._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newPushover = await tx + .insert(pushover) + .values({ + userKey: input.userKey, + apiToken: input.apiToken, + priority: input.priority, + retry: input.retry, + expire: input.expire, + }) + .returning() + .then((value) => value[0]); + + if (!newPushover) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting pushover", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + pushoverId: newPushover.pushoverId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + serverThreshold: input.serverThreshold, + notificationType: "pushover", + organizationId: organizationId, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updatePushoverNotification = async ( + input: typeof apiUpdatePushover._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(pushover) + .set({ + userKey: input.userKey, + apiToken: input.apiToken, + priority: input.priority, + retry: input.retry, + expire: input.expire, + }) + .where(eq(pushover.pushoverId, input.pushoverId)); + + return newDestination; + }); +}; diff --git a/packages/server/src/utils/notifications/utils.ts b/packages/server/src/utils/notifications/utils.ts index 72a6b04be..78494ef41 100644 --- a/packages/server/src/utils/notifications/utils.ts +++ b/packages/server/src/utils/notifications/utils.ts @@ -1,8 +1,8 @@ -import type { - custom, - discord, - email, - gotify, +import type { + custom, + discord, + email, + gotify, lark, ntfy, pushover, @@ -10,275 +10,275 @@ import type { slack, telegram, } from "@dokploy/server/db/schema"; -import nodemailer from "nodemailer"; -import { Resend } from "resend"; - -export const sendEmailNotification = async ( - connection: typeof email.$inferInsert, - subject: string, - htmlContent: string, -) => { - try { - const { - smtpServer, - smtpPort, - username, - password, - fromAddress, - toAddresses, - } = connection; - const transporter = nodemailer.createTransport({ - host: smtpServer, - port: smtpPort, - auth: { user: username, pass: password }, - }); - - await transporter.sendMail({ - from: fromAddress, - to: toAddresses.join(", "), - subject, - html: htmlContent, - textEncoding: "base64", - }); - } catch (err) { - console.log(err); - throw new Error( - `Failed to send email notification ${err instanceof Error ? err.message : "Unknown error"}`, - ); - } -}; - -export const sendResendNotification = async ( - connection: typeof resend.$inferInsert, - subject: string, - htmlContent: string, -) => { - try { - const client = new Resend(connection.apiKey); - - const result = await client.emails.send({ - from: connection.fromAddress, - to: connection.toAddresses, - subject, - html: htmlContent, - }); - - if (result.error) { - throw new Error(result.error.message); - } - } catch (err) { - console.log(err); - throw new Error( - `Failed to send Resend notification ${err instanceof Error ? err.message : "Unknown error"}`, - ); - } -}; - -export const sendDiscordNotification = async ( - connection: typeof discord.$inferInsert, - embed: any, -) => { - try { - const response = await fetch(connection.webhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ embeds: [embed] }), - }); - if (!response.ok) { - throw new Error( - `Failed to send discord notification ${response.statusText}`, - ); - } - } catch (err) { - console.log("error", err); - throw new Error( - `Failed to send discord notification ${err instanceof Error ? err.message : "Unknown error"}`, - ); - } -}; - -export const sendTelegramNotification = async ( - connection: typeof telegram.$inferInsert, - messageText: string, - inlineButton?: { - text: string; - url: string; - }[][], -) => { - try { - const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`; - await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - chat_id: connection.chatId, - message_thread_id: connection.messageThreadId, - text: messageText, - parse_mode: "HTML", - disable_web_page_preview: true, - reply_markup: { - inline_keyboard: inlineButton, - }, - }), - }); - } catch (err) { - console.log(err); - } -}; - -export const sendSlackNotification = async ( - connection: typeof slack.$inferInsert, - message: any, -) => { - try { - const response = await fetch(connection.webhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(message), - }); - if (!response.ok) { - throw new Error( - `Failed to send slack notification ${response.statusText}`, - ); - } - } catch (err) { - console.log("error", err); - throw new Error( - `Failed to send slack notification ${err instanceof Error ? err.message : "Unknown error"}`, - ); - } -}; - -export const sendGotifyNotification = async ( - connection: typeof gotify.$inferInsert, - title: string, - message: string, -) => { - const response = await fetch(`${connection.serverUrl}/message`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Gotify-Key": connection.appToken, - }, - body: JSON.stringify({ - title: title, - message: message, - priority: connection.priority, - extras: { - "client::display": { - contentType: "text/plain", - }, - }, - }), - }); - - if (!response.ok) { - throw new Error( - `Failed to send Gotify notification: ${response.statusText}`, - ); - } -}; - -export const sendNtfyNotification = async ( - connection: typeof ntfy.$inferInsert, - title: string, - tags: string, - actions: string, - message: string, -) => { - const response = await fetch(`${connection.serverUrl}/${connection.topic}`, { - method: "POST", - headers: { - ...(connection.accessToken && { - Authorization: `Bearer ${connection.accessToken}`, - }), - "X-Priority": connection.priority?.toString() || "3", - "X-Title": title, - "X-Tags": tags, - "X-Actions": actions, - }, - body: message, - }); - - if (!response.ok) { - throw new Error(`Failed to send ntfy notification: ${response.statusText}`); - } -}; - -export const sendCustomNotification = async ( - connection: typeof custom.$inferInsert, - payload: Record, -) => { - try { - // Merge default headers with custom headers (now already an object from jsonb) - const headers: Record = { - "Content-Type": "application/json", - ...(connection.headers || {}), - }; - - // Default body with payload - const body = JSON.stringify(payload); - - const response = await fetch(connection.endpoint, { - method: "POST", - headers, - body, - }); - - if (!response.ok) { - throw new Error( - `Failed to send custom notification: ${response.statusText}`, - ); - } - - return response; - } catch (error) { - console.error("Error sending custom notification:", error); - throw error; - } -}; - -export const sendLarkNotification = async ( - connection: typeof lark.$inferInsert, - message: any, -) => { - try { - await fetch(connection.webhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(message), - }); - } catch (err) { - console.log(err); - } -}; - -export const sendPushoverNotification = async ( - connection: typeof pushover.$inferInsert, - title: string, - message: string, -) => { - const formData = new URLSearchParams(); - formData.append("token", connection.apiToken); - formData.append("user", connection.userKey); - formData.append("title", title); - formData.append("message", message); - formData.append("priority", connection.priority?.toString() || "0"); - - // For emergency priority (2), retry and expire are required - if (connection.priority === 2) { - formData.append("retry", connection.retry?.toString() || "30"); - formData.append("expire", connection.expire?.toString() || "3600"); - } - - const response = await fetch("https://api.pushover.net/1/messages.json", { - method: "POST", - body: formData, - }); - - if (!response.ok) { - throw new Error( - `Failed to send Pushover notification: ${response.statusText}`, - ); - } -}; +import nodemailer from "nodemailer"; +import { Resend } from "resend"; + +export const sendEmailNotification = async ( + connection: typeof email.$inferInsert, + subject: string, + htmlContent: string, +) => { + try { + const { + smtpServer, + smtpPort, + username, + password, + fromAddress, + toAddresses, + } = connection; + const transporter = nodemailer.createTransport({ + host: smtpServer, + port: smtpPort, + auth: { user: username, pass: password }, + }); + + await transporter.sendMail({ + from: fromAddress, + to: toAddresses.join(", "), + subject, + html: htmlContent, + textEncoding: "base64", + }); + } catch (err) { + console.log(err); + throw new Error( + `Failed to send email notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); + } +}; + +export const sendResendNotification = async ( + connection: typeof resend.$inferInsert, + subject: string, + htmlContent: string, +) => { + try { + const client = new Resend(connection.apiKey); + + const result = await client.emails.send({ + from: connection.fromAddress, + to: connection.toAddresses, + subject, + html: htmlContent, + }); + + if (result.error) { + throw new Error(result.error.message); + } + } catch (err) { + console.log(err); + throw new Error( + `Failed to send Resend notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); + } +}; + +export const sendDiscordNotification = async ( + connection: typeof discord.$inferInsert, + embed: any, +) => { + try { + const response = await fetch(connection.webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ embeds: [embed] }), + }); + if (!response.ok) { + throw new Error( + `Failed to send discord notification ${response.statusText}`, + ); + } + } catch (err) { + console.log("error", err); + throw new Error( + `Failed to send discord notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); + } +}; + +export const sendTelegramNotification = async ( + connection: typeof telegram.$inferInsert, + messageText: string, + inlineButton?: { + text: string; + url: string; + }[][], +) => { + try { + const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`; + await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: connection.chatId, + message_thread_id: connection.messageThreadId, + text: messageText, + parse_mode: "HTML", + disable_web_page_preview: true, + reply_markup: { + inline_keyboard: inlineButton, + }, + }), + }); + } catch (err) { + console.log(err); + } +}; + +export const sendSlackNotification = async ( + connection: typeof slack.$inferInsert, + message: any, +) => { + try { + const response = await fetch(connection.webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(message), + }); + if (!response.ok) { + throw new Error( + `Failed to send slack notification ${response.statusText}`, + ); + } + } catch (err) { + console.log("error", err); + throw new Error( + `Failed to send slack notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); + } +}; + +export const sendGotifyNotification = async ( + connection: typeof gotify.$inferInsert, + title: string, + message: string, +) => { + const response = await fetch(`${connection.serverUrl}/message`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Gotify-Key": connection.appToken, + }, + body: JSON.stringify({ + title: title, + message: message, + priority: connection.priority, + extras: { + "client::display": { + contentType: "text/plain", + }, + }, + }), + }); + + if (!response.ok) { + throw new Error( + `Failed to send Gotify notification: ${response.statusText}`, + ); + } +}; + +export const sendNtfyNotification = async ( + connection: typeof ntfy.$inferInsert, + title: string, + tags: string, + actions: string, + message: string, +) => { + const response = await fetch(`${connection.serverUrl}/${connection.topic}`, { + method: "POST", + headers: { + ...(connection.accessToken && { + Authorization: `Bearer ${connection.accessToken}`, + }), + "X-Priority": connection.priority?.toString() || "3", + "X-Title": title, + "X-Tags": tags, + "X-Actions": actions, + }, + body: message, + }); + + if (!response.ok) { + throw new Error(`Failed to send ntfy notification: ${response.statusText}`); + } +}; + +export const sendCustomNotification = async ( + connection: typeof custom.$inferInsert, + payload: Record, +) => { + try { + // Merge default headers with custom headers (now already an object from jsonb) + const headers: Record = { + "Content-Type": "application/json", + ...(connection.headers || {}), + }; + + // Default body with payload + const body = JSON.stringify(payload); + + const response = await fetch(connection.endpoint, { + method: "POST", + headers, + body, + }); + + if (!response.ok) { + throw new Error( + `Failed to send custom notification: ${response.statusText}`, + ); + } + + return response; + } catch (error) { + console.error("Error sending custom notification:", error); + throw error; + } +}; + +export const sendLarkNotification = async ( + connection: typeof lark.$inferInsert, + message: any, +) => { + try { + await fetch(connection.webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(message), + }); + } catch (err) { + console.log(err); + } +}; + +export const sendPushoverNotification = async ( + connection: typeof pushover.$inferInsert, + title: string, + message: string, +) => { + const formData = new URLSearchParams(); + formData.append("token", connection.apiToken); + formData.append("user", connection.userKey); + formData.append("title", title); + formData.append("message", message); + formData.append("priority", connection.priority?.toString() || "0"); + + // For emergency priority (2), retry and expire are required + if (connection.priority === 2) { + formData.append("retry", connection.retry?.toString() || "30"); + formData.append("expire", connection.expire?.toString() || "3600"); + } + + const response = await fetch("https://api.pushover.net/1/messages.json", { + method: "POST", + body: formData, + }); + + if (!response.ok) { + throw new Error( + `Failed to send Pushover notification: ${response.statusText}`, + ); + } +}; diff --git a/packages/server/src/utils/notifications/volume-backup.ts b/packages/server/src/utils/notifications/volume-backup.ts index 611ccaf85..bb225f973 100644 --- a/packages/server/src/utils/notifications/volume-backup.ts +++ b/packages/server/src/utils/notifications/volume-backup.ts @@ -1,11 +1,11 @@ -import { db } from "@dokploy/server/db"; -import { notifications } from "@dokploy/server/db/schema"; -import { VolumeBackupEmail } from "@dokploy/server/emails/emails/volume-backup"; -import { renderAsync } from "@react-email/components"; -import { format } from "date-fns"; -import { and, eq } from "drizzle-orm"; -import { - sendDiscordNotification, +import { db } from "@dokploy/server/db"; +import { notifications } from "@dokploy/server/db/schema"; +import { VolumeBackupEmail } from "@dokploy/server/emails/emails/volume-backup"; +import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; +import { and, eq } from "drizzle-orm"; +import { + sendDiscordNotification, sendEmailNotification, sendGotifyNotification, sendNtfyNotification, @@ -14,287 +14,279 @@ import { sendSlackNotification, sendTelegramNotification, } from "./utils"; - -export const sendVolumeBackupNotifications = async ({ - projectName, - applicationName, - volumeName, - serviceType, - type, - errorMessage, - organizationId, - backupSize, -}: { - projectName: string; - applicationName: string; - volumeName: string; - serviceType: - | "application" - | "postgres" - | "mysql" - | "mongodb" - | "mariadb" - | "redis" - | "compose"; - type: "error" | "success"; - organizationId: string; - errorMessage?: string; - backupSize?: string; -}) => { - const date = new Date(); - const unixDate = ~~(Number(date) / 1000); - const notificationList = await db.query.notifications.findMany({ - where: and( - eq(notifications.volumeBackup, true), - eq(notifications.organizationId, organizationId), - ), - with: { - email: true, - discord: true, - telegram: true, - slack: true, - resend: true, - gotify: true, - ntfy: true, - pushover: true, - }, - }); - - for (const notification of notificationList) { - const { - email, - resend, - discord, - telegram, - slack, - gotify, - ntfy, - pushover, - } = notification; - - if (email || resend) { - const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`; - const htmlContent = await renderAsync( - VolumeBackupEmail({ - projectName, - applicationName, - volumeName, - serviceType, - type, - errorMessage, - backupSize, - date: date.toISOString(), - }), - ); - if (email) { - await sendEmailNotification(email, subject, htmlContent); - } - if (resend) { - await sendResendNotification(resend, subject, htmlContent); - } - } - - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); - - await sendDiscordNotification(discord, { - title: - type === "success" - ? decorate(">", "`✅` Volume Backup Successful") - : decorate(">", "`❌` Volume Backup Failed"), - color: type === "success" ? 0x57f287 : 0xed4245, - fields: [ - { - name: decorate("`🛠️`", "Project"), - value: projectName, - inline: true, - }, - { - name: decorate("`⚙️`", "Application"), - value: applicationName, - inline: true, - }, - { - name: decorate("`💾`", "Volume Name"), - value: volumeName, - inline: true, - }, - { - name: decorate("`🔧`", "Service Type"), - value: serviceType, - inline: true, - }, - ...(backupSize - ? [ - { - name: decorate("`📊`", "Backup Size"), - value: backupSize, - inline: true, - }, - ] - : []), - { - name: decorate("`📅`", "Date"), - value: ``, - inline: true, - }, - { - name: decorate("`⌚`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`❓`", "Type"), - value: type - .replace("error", "Failed") - .replace("success", "Successful"), - inline: true, - }, - ...(type === "error" && errorMessage - ? [ - { - name: decorate("`⚠️`", "Error Message"), - value: `\`\`\`${errorMessage}\`\`\``, - }, - ] - : []), - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Volume Backup Notification", - }, - }); - } - - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - - await sendGotifyNotification( - gotify, - decorate( - type === "success" ? "✅" : "❌", - `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, - ), - `${decorate("🛠️", `Project: ${projectName}`)}` + - `${decorate("⚙️", `Application: ${applicationName}`)}` + - `${decorate("💾", `Volume Name: ${volumeName}`)}` + - `${decorate("🔧", `Service Type: ${serviceType}`)}` + - `${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` + - `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + - `${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`, - ); - } - - if (ntfy) { - await sendNtfyNotification( - ntfy, - `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, - `${type === "success" ? "white_check_mark" : "x"}`, - "", - `🛠️Project: ${projectName}\n` + - `⚙️Application: ${applicationName}\n` + - `💾Volume Name: ${volumeName}\n` + - `🔧Service Type: ${serviceType}\n` + - `${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` + - `🕒Date: ${date.toLocaleString()}\n` + - `${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`, - ); - } - - if (telegram) { - const isError = type === "error" && errorMessage; - - const statusEmoji = type === "success" ? "✅" : "❌"; - const typeStatus = type === "success" ? "Successful" : "Failed"; - const errorMsg = isError - ? `\n\nError:\n
${errorMessage}
` - : ""; - const sizeInfo = backupSize ? `\nBackup Size: ${backupSize}` : ""; - - const messageText = `${statusEmoji} Volume Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nVolume Name: ${volumeName}\nService Type: ${serviceType}${sizeInfo}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; - - await sendTelegramNotification(telegram, messageText); - } - - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: type === "success" ? "#00FF00" : "#FF0000", - pretext: - type === "success" - ? ":white_check_mark: *Volume Backup Successful*" - : ":x: *Volume Backup Failed*", - fields: [ - ...(type === "error" && errorMessage - ? [ - { - title: "Error Message", - value: errorMessage, - short: false, - }, - ] - : []), - { - title: "Project", - value: projectName, - short: true, - }, - { - title: "Application", - value: applicationName, - short: true, - }, - { - title: "Volume Name", - value: volumeName, - short: true, - }, - { - title: "Service Type", - value: serviceType, - short: true, - }, - ...(backupSize - ? [ - { - title: "Backup Size", - value: backupSize, - short: true, - }, - ] - : []), - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - { - title: "Type", - value: type, - short: true, - }, - { - title: "Status", - value: type === "success" ? "Successful" : "Failed", - short: true, - }, - ], - }, - ], - }); - } - - if (pushover) { - await sendPushoverNotification( - pushover, - `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, - `Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`, - ); - } - } -}; + +export const sendVolumeBackupNotifications = async ({ + projectName, + applicationName, + volumeName, + serviceType, + type, + errorMessage, + organizationId, + backupSize, +}: { + projectName: string; + applicationName: string; + volumeName: string; + serviceType: + | "application" + | "postgres" + | "mysql" + | "mongodb" + | "mariadb" + | "redis" + | "compose"; + type: "error" | "success"; + organizationId: string; + errorMessage?: string; + backupSize?: string; +}) => { + const date = new Date(); + const unixDate = ~~(Number(date) / 1000); + const notificationList = await db.query.notifications.findMany({ + where: and( + eq(notifications.volumeBackup, true), + eq(notifications.organizationId, organizationId), + ), + with: { + email: true, + discord: true, + telegram: true, + slack: true, + resend: true, + gotify: true, + ntfy: true, + pushover: true, + }, + }); + + for (const notification of notificationList) { + const { email, resend, discord, telegram, slack, gotify, ntfy, pushover } = + notification; + + if (email || resend) { + const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`; + const htmlContent = await renderAsync( + VolumeBackupEmail({ + projectName, + applicationName, + volumeName, + serviceType, + type, + errorMessage, + backupSize, + date: date.toISOString(), + }), + ); + if (email) { + await sendEmailNotification(email, subject, htmlContent); + } + if (resend) { + await sendResendNotification(resend, subject, htmlContent); + } + } + + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + + await sendDiscordNotification(discord, { + title: + type === "success" + ? decorate(">", "`✅` Volume Backup Successful") + : decorate(">", "`❌` Volume Backup Failed"), + color: type === "success" ? 0x57f287 : 0xed4245, + fields: [ + { + name: decorate("`🛠️`", "Project"), + value: projectName, + inline: true, + }, + { + name: decorate("`⚙️`", "Application"), + value: applicationName, + inline: true, + }, + { + name: decorate("`💾`", "Volume Name"), + value: volumeName, + inline: true, + }, + { + name: decorate("`🔧`", "Service Type"), + value: serviceType, + inline: true, + }, + ...(backupSize + ? [ + { + name: decorate("`📊`", "Backup Size"), + value: backupSize, + inline: true, + }, + ] + : []), + { + name: decorate("`📅`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`⌚`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`❓`", "Type"), + value: type + .replace("error", "Failed") + .replace("success", "Successful"), + inline: true, + }, + ...(type === "error" && errorMessage + ? [ + { + name: decorate("`⚠️`", "Error Message"), + value: `\`\`\`${errorMessage}\`\`\``, + }, + ] + : []), + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Volume Backup Notification", + }, + }); + } + + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + + await sendGotifyNotification( + gotify, + decorate( + type === "success" ? "✅" : "❌", + `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, + ), + `${decorate("🛠️", `Project: ${projectName}`)}` + + `${decorate("⚙️", `Application: ${applicationName}`)}` + + `${decorate("💾", `Volume Name: ${volumeName}`)}` + + `${decorate("🔧", `Service Type: ${serviceType}`)}` + + `${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` + + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + + `${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`, + ); + } + + if (ntfy) { + await sendNtfyNotification( + ntfy, + `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, + `${type === "success" ? "white_check_mark" : "x"}`, + "", + `🛠️Project: ${projectName}\n` + + `⚙️Application: ${applicationName}\n` + + `💾Volume Name: ${volumeName}\n` + + `🔧Service Type: ${serviceType}\n` + + `${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` + + `🕒Date: ${date.toLocaleString()}\n` + + `${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`, + ); + } + + if (telegram) { + const isError = type === "error" && errorMessage; + + const statusEmoji = type === "success" ? "✅" : "❌"; + const typeStatus = type === "success" ? "Successful" : "Failed"; + const errorMsg = isError + ? `\n\nError:\n
${errorMessage}
` + : ""; + const sizeInfo = backupSize ? `\nBackup Size: ${backupSize}` : ""; + + const messageText = `${statusEmoji} Volume Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nVolume Name: ${volumeName}\nService Type: ${serviceType}${sizeInfo}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; + + await sendTelegramNotification(telegram, messageText); + } + + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: type === "success" ? "#00FF00" : "#FF0000", + pretext: + type === "success" + ? ":white_check_mark: *Volume Backup Successful*" + : ":x: *Volume Backup Failed*", + fields: [ + ...(type === "error" && errorMessage + ? [ + { + title: "Error Message", + value: errorMessage, + short: false, + }, + ] + : []), + { + title: "Project", + value: projectName, + short: true, + }, + { + title: "Application", + value: applicationName, + short: true, + }, + { + title: "Volume Name", + value: volumeName, + short: true, + }, + { + title: "Service Type", + value: serviceType, + short: true, + }, + ...(backupSize + ? [ + { + title: "Backup Size", + value: backupSize, + short: true, + }, + ] + : []), + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + { + title: "Type", + value: type, + short: true, + }, + { + title: "Status", + value: type === "success" ? "Successful" : "Failed", + short: true, + }, + ], + }, + ], + }); + } + + if (pushover) { + await sendPushoverNotification( + pushover, + `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, + `Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`, + ); + } + } +};