From 9954d5b209d86292ba6f40d3e9d9bfab11cdae15 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 19 Jul 2024 21:57:46 -0600 Subject: [PATCH] refactor(notification): split functions for each notification actio --- server/api/services/application.ts | 8 +- server/api/services/notification.ts | 706 ------------------ server/server.ts | 2 +- server/utils/backups/mariadb.ts | 2 +- server/utils/backups/mongo.ts | 2 +- server/utils/backups/mysql.ts | 2 +- server/utils/backups/postgres.ts | 2 +- server/utils/notifications/build-error.ts | 160 ++++ server/utils/notifications/build-success.ts | 150 ++++ server/utils/notifications/database-backup.ts | 183 +++++ server/utils/notifications/docker-cleanup.ts | 97 +++ server/utils/notifications/dokploy-restart.ts | 91 +++ server/utils/notifications/utils.ts | 69 ++ 13 files changed, 758 insertions(+), 716 deletions(-) create mode 100644 server/utils/notifications/build-error.ts create mode 100644 server/utils/notifications/build-success.ts create mode 100644 server/utils/notifications/database-backup.ts create mode 100644 server/utils/notifications/docker-cleanup.ts create mode 100644 server/utils/notifications/dokploy-restart.ts create mode 100644 server/utils/notifications/utils.ts diff --git a/server/api/services/application.ts b/server/api/services/application.ts index 67fb68c78..10040e555 100644 --- a/server/api/services/application.ts +++ b/server/api/services/application.ts @@ -17,10 +17,9 @@ import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { findAdmin } from "./admin"; import { createDeployment, updateDeploymentStatus } from "./deployment"; -import { - sendBuildErrorNotifications, - sendBuildSuccessNotifications, -} from "./notification"; + +import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error"; +import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success"; import { validUniqueServerAppName } from "./project"; export type Application = typeof applications.$inferSelect; @@ -168,7 +167,6 @@ export const deployApplication = async ({ buildLink: deployment.logPath, }); } catch (error) { - console.log("Error on build", error); await updateDeploymentStatus(deployment.deploymentId, "error"); await updateApplicationStatus(applicationId, "error"); await sendBuildErrorNotifications({ diff --git a/server/api/services/notification.ts b/server/api/services/notification.ts index cbbb72746..31ff91ea2 100644 --- a/server/api/services/notification.ts +++ b/server/api/services/notification.ts @@ -1,18 +1,9 @@ -import { BuildFailedEmail } from "@/emails/emails/build-failed"; -import BuildSuccessEmail from "@/emails/emails/build-success"; -import DatabaseBackupEmail from "@/emails/emails/database-backup"; -import DockerCleanupEmail from "@/emails/emails/docker-cleanup"; -import DokployRestartEmail from "@/emails/emails/dokploy-restart"; import { db } from "@/server/db"; import { type apiCreateDiscord, type apiCreateEmail, type apiCreateSlack, type apiCreateTelegram, - type apiTestDiscordConnection, - type apiTestEmailConnection, - type apiTestSlackConnection, - type apiTestTelegramConnection, type apiUpdateDiscord, type apiUpdateEmail, type apiUpdateSlack, @@ -23,11 +14,8 @@ import { slack, telegram, } from "@/server/db/schema"; -import { render } from "@react-email/components"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; -import nodemailer from "nodemailer"; -import type SMTPTransport from "nodemailer/lib/smtp-transport"; export type Notification = typeof notifications.$inferSelect; @@ -419,697 +407,3 @@ export const updateDestinationById = async ( return result[0]; }; - -interface BuildFailedEmailProps { - projectName: string; - applicationName: string; - applicationType: string; - errorMessage: string; - buildLink: string; -} - -export const sendBuildErrorNotifications = async ({ - projectName, - applicationName, - applicationType, - errorMessage, - buildLink, -}: BuildFailedEmailProps) => { - const date = new Date(); - const notificationList = await db.query.notifications.findMany({ - where: eq(notifications.appBuildError, true), - with: { - email: true, - discord: true, - telegram: true, - slack: true, - }, - }); - - for (const notification of notificationList) { - const { email, discord, telegram, slack } = notification; - if (email) { - await sendEmailNotification( - email, - "Build failed for dokploy", - render( - BuildFailedEmail({ - projectName, - applicationName, - applicationType, - errorMessage, - buildLink, - date: date.toLocaleString(), - }), - ), - ); - } - - if (discord) { - const { webhookUrl } = discord; - await sendDiscordNotification(discord, { - title: "⚠️ Build Failed", - color: 0xff0000, - fields: [ - { - name: "Project", - value: projectName, - inline: true, - }, - { - name: "Application", - value: applicationName, - inline: true, - }, - { - name: "Type", - value: applicationType, - inline: true, - }, - { - name: "Error", - value: errorMessage, - }, - { - name: "Build Link", - value: buildLink, - }, - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Build Notification", - }, - }); - } - - if (telegram) { - await sendTelegramNotification( - telegram, - ` - ⚠️ Build Failed - - Project: ${projectName} - Application: ${applicationName} - Type: ${applicationType} - Time: ${date.toLocaleString()} - - Error: -
${errorMessage}
-
- Build Details: ${buildLink}
- `,
- );
- }
-
- if (slack) {
- const { channel } = slack;
- await sendSlackNotification(slack, {
- channel: channel,
- attachments: [
- {
- color: "#FF0000",
- pretext: ":warning: *Build Failed*",
- fields: [
- {
- title: "Project",
- value: projectName,
- short: true,
- },
- {
- title: "Application",
- value: applicationName,
- short: true,
- },
- {
- title: "Type",
- value: applicationType,
- short: true,
- },
- {
- title: "Time",
- value: date.toLocaleString(),
- short: true,
- },
- {
- title: "Error",
- value: `\`\`\`${errorMessage}\`\`\``,
- short: false,
- },
- ],
- actions: [
- {
- type: "button",
- text: "View Build Details",
- url: "https://doks.dev/build-details",
- },
- ],
- },
- ],
- });
- }
- }
-};
-
-interface BuildSuccessEmailProps {
- projectName: string;
- applicationName: string;
- applicationType: string;
- buildLink: string;
-}
-
-export const sendBuildSuccessNotifications = async ({
- projectName,
- applicationName,
- applicationType,
- buildLink,
-}: BuildSuccessEmailProps) => {
- const date = new Date();
- const notificationList = await db.query.notifications.findMany({
- where: eq(notifications.appDeploy, true),
- with: {
- email: true,
- discord: true,
- telegram: true,
- slack: true,
- },
- });
-
- for (const notification of notificationList) {
- const { email, discord, telegram, slack } = notification;
-
- if (email) {
- await sendEmailNotification(
- email,
- "Build success for dokploy",
- render(
- BuildSuccessEmail({
- projectName,
- applicationName,
- applicationType,
- buildLink,
- date: date.toLocaleString(),
- }),
- ),
- );
- }
-
- if (discord) {
- await sendDiscordNotification(discord, {
- title: "✅ Build Success",
- color: 0x00ff00,
- fields: [
- {
- name: "Project",
- value: projectName,
- inline: true,
- },
- {
- name: "Application",
- value: applicationName,
- inline: true,
- },
- {
- name: "Type",
- value: applicationType,
- inline: true,
- },
- {
- name: "Build Link",
- value: buildLink,
- },
- ],
- timestamp: date.toISOString(),
- footer: {
- text: "Dokploy Build Notification",
- },
- });
- }
-
- if (telegram) {
- await sendTelegramNotification(
- telegram,
- `
- ✅ Build Success
-
- Project: ${projectName}
- Application: ${applicationName}
- Type: ${applicationType}
- Time: ${date.toLocaleString()}
-
- Build Details: ${buildLink}
- `,
- );
- }
-
- if (slack) {
- const { webhookUrl, channel } = slack;
- await sendSlackNotification(slack, {
- channel: channel,
- attachments: [
- {
- color: "#00FF00",
- pretext: ":white_check_mark: *Build Success*",
- fields: [
- {
- title: "Project",
- value: projectName,
- short: true,
- },
- {
- title: "Application",
- value: applicationName,
- short: true,
- },
- {
- title: "Type",
- value: applicationType,
- short: true,
- },
- {
- title: "Time",
- value: date.toLocaleString(),
- short: true,
- },
- {
- title: "Build Link",
- value: buildLink,
- },
- ],
- actions: [
- {
- type: "button",
- text: "View Build Details",
- url: "https://doks.dev/build-details",
- },
- ],
- },
- ],
- });
- }
- }
-};
-
-export const sendDatabaseBackupNotifications = async ({
- projectName,
- applicationName,
- databaseType,
- type,
- errorMessage,
-}: {
- projectName: string;
- applicationName: string;
- databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
- type: "error" | "success";
- errorMessage?: string;
-}) => {
- const date = new Date();
- const notificationList = await db.query.notifications.findMany({
- where: eq(notifications.databaseBackup, true),
- with: {
- email: true,
- discord: true,
- telegram: true,
- slack: true,
- },
- });
-
- for (const notification of notificationList) {
- const { email, discord, telegram, slack } = notification;
-
- if (email) {
- await sendEmailNotification(
- email,
- "Database backup for dokploy",
- render(
- DatabaseBackupEmail({
- projectName,
- applicationName,
- databaseType,
- type,
- errorMessage,
- date: date.toLocaleString(),
- }),
- ),
- );
- }
-
- if (discord) {
- await sendDiscordNotification(discord, {
- title:
- type === "success"
- ? "✅ Database Backup Successful"
- : "❌ Database Backup Failed",
- color: type === "success" ? 0x00ff00 : 0xff0000,
- fields: [
- {
- name: "Project",
- value: projectName,
- inline: true,
- },
- {
- name: "Application",
- value: applicationName,
- inline: true,
- },
- {
- name: "Type",
- value: databaseType,
- inline: true,
- },
- {
- name: "Time",
- value: date.toLocaleString(),
- inline: true,
- },
- {
- name: "Type",
- value: type,
- },
- ...(type === "error" && errorMessage
- ? [
- {
- name: "Error Message",
- value: errorMessage,
- },
- ]
- : []),
- ],
- timestamp: date.toISOString(),
- footer: {
- text: "Dokploy Database Backup Notification",
- },
- });
- }
-
- if (telegram) {
- const statusEmoji = type === "success" ? "✅" : "❌";
- const messageText = `
- ${statusEmoji} Database Backup ${type === "success" ? "Successful" : "Failed"}
-
- Project: ${projectName}
- Application: ${applicationName}
- Type: ${databaseType}
- Time: ${date.toLocaleString()}
-
- Status: ${type === "success" ? "Successful" : "Failed"}
- ${type === "error" && errorMessage ? `Error: ${errorMessage}` : ""}
- `;
- 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: *Database Backup Successful*"
- : ":x: *Database 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: "Type",
- value: databaseType,
- short: true,
- },
- {
- title: "Time",
- value: date.toLocaleString(),
- short: true,
- },
- {
- title: "Type",
- value: type,
- },
- {
- title: "Status",
- value: type === "success" ? "Successful" : "Failed",
- },
- ],
- actions: [
- {
- type: "button",
- text: "View Build Details",
- url: "https://doks.dev/build-details",
- },
- ],
- },
- ],
- });
- }
- }
-};
-
-export const sendDockerCleanupNotifications = async (
- message = "Docker cleanup for dokploy",
-) => {
- const date = new Date();
- const notificationList = await db.query.notifications.findMany({
- where: eq(notifications.dockerCleanup, true),
- with: {
- email: true,
- discord: true,
- telegram: true,
- slack: true,
- },
- });
-
- for (const notification of notificationList) {
- const { email, discord, telegram, slack } = notification;
-
- if (email) {
- await sendEmailNotification(
- email,
- "Docker cleanup for dokploy",
- render(DockerCleanupEmail({ message, date: date.toLocaleString() })),
- );
- }
-
- if (discord) {
- await sendDiscordNotification(discord, {
- title: "✅ Docker Cleanup",
- color: 0x00ff00,
- fields: [
- {
- name: "Message",
- value: message,
- },
- ],
- timestamp: date.toISOString(),
- footer: {
- text: "Dokploy Docker Cleanup Notification",
- },
- });
- }
-
- if (telegram) {
- await sendTelegramNotification(
- telegram,
- `
- ✅ Docker Cleanup
- Message: ${message}
- Time: ${date.toLocaleString()}
- `,
- );
- }
-
- if (slack) {
- const { channel } = slack;
- await sendSlackNotification(slack, {
- channel: channel,
- attachments: [
- {
- color: "#00FF00",
- pretext: ":white_check_mark: *Docker Cleanup*",
- fields: [
- {
- title: "Message",
- value: message,
- },
- {
- title: "Time",
- value: date.toLocaleString(),
- short: true,
- },
- ],
- actions: [
- {
- type: "button",
- text: "View Build Details",
- url: "https://doks.dev/build-details",
- },
- ],
- },
- ],
- });
- }
- }
-};
-
-export const sendEmailNotification = async (
- connection: typeof email.$inferInsert,
- subject: string,
- htmlContent: string,
-) => {
- const { smtpServer, smtpPort, username, password, fromAddress, toAddresses } =
- connection;
- const transporter = nodemailer.createTransport({
- host: smtpServer,
- port: smtpPort,
- secure: smtpPort === 465,
- auth: { user: username, pass: password },
- });
-
- await transporter.sendMail({
- from: fromAddress,
- to: toAddresses.join(", "),
- subject,
- html: htmlContent,
- });
-};
-
-export const sendDiscordNotification = async (
- connection: typeof discord.$inferInsert,
- embed: any,
-) => {
- 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");
-};
-
-export const sendTelegramNotification = async (
- connection: typeof telegram.$inferInsert,
- messageText: string,
-) => {
- const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
- const response = await fetch(url, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- chat_id: connection.chatId,
- text: messageText,
- parse_mode: "HTML",
- disable_web_page_preview: true,
- }),
- });
-
- if (!response.ok) throw new Error("Failed to send Telegram notification");
-};
-
-export const sendSlackNotification = async (
- connection: typeof slack.$inferInsert,
- message: any,
-) => {
- 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");
-};
-
-export const sendDokployRestartNotifications = async () => {
- const date = new Date();
- const notificationList = await db.query.notifications.findMany({
- where: eq(notifications.dokployRestart, true),
- with: {
- email: true,
- discord: true,
- telegram: true,
- slack: true,
- },
- });
-
- for (const notification of notificationList) {
- const { email, discord, telegram, slack } = notification;
-
- if (email) {
- await sendEmailNotification(
- email,
- "Dokploy Server Restarted",
- render(DokployRestartEmail({ date: date.toLocaleString() })),
- );
- }
-
- if (discord) {
- await sendDiscordNotification(discord, {
- title: "✅ Dokploy Server Restarted",
- color: 0x00ff00,
- fields: [
- {
- name: "Time",
- value: date.toLocaleString(),
- inline: true,
- },
- ],
- timestamp: date.toISOString(),
- footer: {
- text: "Dokploy Restart Notification",
- },
- });
- }
-
- if (telegram) {
- await sendTelegramNotification(
- telegram,
- `
- ✅ Dokploy Serverd Restarted
- Time: ${date.toLocaleString()}
- `,
- );
- }
-
- if (slack) {
- const { channel } = slack;
- await sendSlackNotification(slack, {
- channel: channel,
- attachments: [
- {
- color: "#FF0000",
- pretext: ":white_check_mark: *Dokploy Server Restarted*",
- fields: [
- {
- title: "Time",
- value: date.toLocaleString(),
- short: true,
- },
- ],
- actions: [
- {
- type: "button",
- text: "View Build Details",
- url: "https://doks.dev/build-details",
- },
- ],
- },
- ],
- });
- }
- }
-};
diff --git a/server/server.ts b/server/server.ts
index 0e8649cf3..1f6b5c4fa 100644
--- a/server/server.ts
+++ b/server/server.ts
@@ -2,7 +2,6 @@ import http from "node:http";
import { migration } from "@/server/db/migration";
import { config } from "dotenv";
import next from "next";
-import { sendDokployRestartNotifications } from "./api/services/notification";
import { deploymentWorker } from "./queues/deployments-queue";
import { setupDirectories } from "./setup/config-paths";
import { initializePostgres } from "./setup/postgres-setup";
@@ -15,6 +14,7 @@ import {
initializeTraefik,
} from "./setup/traefik-setup";
import { initCronJobs } from "./utils/backups";
+import { sendDokployRestartNotifications } from "./utils/notifications/dokploy-restart";
import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-logs";
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
diff --git a/server/utils/backups/mariadb.ts b/server/utils/backups/mariadb.ts
index 518816bd4..9277b2257 100644
--- a/server/utils/backups/mariadb.ts
+++ b/server/utils/backups/mariadb.ts
@@ -2,9 +2,9 @@ import { unlink } from "node:fs/promises";
import path from "node:path";
import type { BackupSchedule } from "@/server/api/services/backup";
import type { Mariadb } from "@/server/api/services/mariadb";
-import { sendDatabaseBackupNotifications } from "@/server/api/services/notification";
import { findProjectById } from "@/server/api/services/project";
import { getServiceContainer } from "../docker/utils";
+import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync } from "../process/execAsync";
import { uploadToS3 } from "./utils";
diff --git a/server/utils/backups/mongo.ts b/server/utils/backups/mongo.ts
index eac8dc5cf..1329b4f40 100644
--- a/server/utils/backups/mongo.ts
+++ b/server/utils/backups/mongo.ts
@@ -2,9 +2,9 @@ import { unlink } from "node:fs/promises";
import path from "node:path";
import type { BackupSchedule } from "@/server/api/services/backup";
import type { Mongo } from "@/server/api/services/mongo";
-import { sendDatabaseBackupNotifications } from "@/server/api/services/notification";
import { findProjectById } from "@/server/api/services/project";
import { getServiceContainer } from "../docker/utils";
+import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync } from "../process/execAsync";
import { uploadToS3 } from "./utils";
diff --git a/server/utils/backups/mysql.ts b/server/utils/backups/mysql.ts
index 5f09a0983..e662dbe1c 100644
--- a/server/utils/backups/mysql.ts
+++ b/server/utils/backups/mysql.ts
@@ -2,9 +2,9 @@ import { unlink } from "node:fs/promises";
import path from "node:path";
import type { BackupSchedule } from "@/server/api/services/backup";
import type { MySql } from "@/server/api/services/mysql";
-import { sendDatabaseBackupNotifications } from "@/server/api/services/notification";
import { findProjectById } from "@/server/api/services/project";
import { getServiceContainer } from "../docker/utils";
+import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync } from "../process/execAsync";
import { uploadToS3 } from "./utils";
diff --git a/server/utils/backups/postgres.ts b/server/utils/backups/postgres.ts
index a6371334e..5c435a9b2 100644
--- a/server/utils/backups/postgres.ts
+++ b/server/utils/backups/postgres.ts
@@ -1,10 +1,10 @@
import { unlink } from "node:fs/promises";
import path from "node:path";
import type { BackupSchedule } from "@/server/api/services/backup";
-import { sendDatabaseBackupNotifications } from "@/server/api/services/notification";
import type { Postgres } from "@/server/api/services/postgres";
import { findProjectById } from "@/server/api/services/project";
import { getServiceContainer } from "../docker/utils";
+import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync } from "../process/execAsync";
import { uploadToS3 } from "./utils";
diff --git a/server/utils/notifications/build-error.ts b/server/utils/notifications/build-error.ts
new file mode 100644
index 000000000..534823658
--- /dev/null
+++ b/server/utils/notifications/build-error.ts
@@ -0,0 +1,160 @@
+import BuildFailedEmail from "@/emails/emails/build-failed";
+import { db } from "@/server/db";
+import { notifications } from "@/server/db/schema";
+import { render } from "@react-email/components";
+import { eq } from "drizzle-orm";
+import {
+ sendDiscordNotification,
+ sendEmailNotification,
+ sendSlackNotification,
+ sendTelegramNotification,
+} from "./utils";
+
+interface Props {
+ projectName: string;
+ applicationName: string;
+ applicationType: string;
+ errorMessage: string;
+ buildLink: string;
+}
+
+export const sendBuildErrorNotifications = async ({
+ projectName,
+ applicationName,
+ applicationType,
+ errorMessage,
+ buildLink,
+}: Props) => {
+ const date = new Date();
+ const notificationList = await db.query.notifications.findMany({
+ where: eq(notifications.appBuildError, true),
+ with: {
+ email: true,
+ discord: true,
+ telegram: true,
+ slack: true,
+ },
+ });
+
+ for (const notification of notificationList) {
+ const { email, discord, telegram, slack } = notification;
+ if (email) {
+ await sendEmailNotification(
+ email,
+ "Build failed for dokploy",
+ render(
+ BuildFailedEmail({
+ projectName,
+ applicationName,
+ applicationType,
+ errorMessage,
+ buildLink,
+ date: date.toLocaleString(),
+ }),
+ ),
+ );
+ }
+
+ if (discord) {
+ await sendDiscordNotification(discord, {
+ title: "⚠️ Build Failed",
+ color: 0xff0000,
+ fields: [
+ {
+ name: "Project",
+ value: projectName,
+ inline: true,
+ },
+ {
+ name: "Application",
+ value: applicationName,
+ inline: true,
+ },
+ {
+ name: "Type",
+ value: applicationType,
+ inline: true,
+ },
+ {
+ name: "Error",
+ value: errorMessage,
+ },
+ {
+ name: "Build Link",
+ value: buildLink,
+ },
+ ],
+ timestamp: date.toISOString(),
+ footer: {
+ text: "Dokploy Build Notification",
+ },
+ });
+ }
+
+ if (telegram) {
+ await sendTelegramNotification(
+ telegram,
+ `
+ ⚠️ Build Failed
+
+ Project: ${projectName}
+ Application: ${applicationName}
+ Type: ${applicationType}
+ Time: ${date.toLocaleString()}
+
+ Error:
+ ${errorMessage}
+
+ Build Details: ${buildLink}
+ `,
+ );
+ }
+
+ if (slack) {
+ const { channel } = slack;
+ await sendSlackNotification(slack, {
+ channel: channel,
+ attachments: [
+ {
+ color: "#FF0000",
+ pretext: ":warning: *Build Failed*",
+ fields: [
+ {
+ title: "Project",
+ value: projectName,
+ short: true,
+ },
+ {
+ title: "Application",
+ value: applicationName,
+ short: true,
+ },
+ {
+ title: "Type",
+ value: applicationType,
+ short: true,
+ },
+ {
+ title: "Time",
+ value: date.toLocaleString(),
+ short: true,
+ },
+ {
+ title: "Error",
+ value: `\`\`\`${errorMessage}\`\`\``,
+ short: false,
+ },
+ ],
+ actions: [
+ {
+ type: "button",
+ text: "View Build Details",
+ url: "https://doks.dev/build-details",
+ },
+ ],
+ },
+ ],
+ });
+ }
+ }
+};
diff --git a/server/utils/notifications/build-success.ts b/server/utils/notifications/build-success.ts
new file mode 100644
index 000000000..e8c8f1069
--- /dev/null
+++ b/server/utils/notifications/build-success.ts
@@ -0,0 +1,150 @@
+import BuildSuccessEmail from "@/emails/emails/build-success";
+import { db } from "@/server/db";
+import { notifications } from "@/server/db/schema";
+import { render } from "@react-email/components";
+import { eq } from "drizzle-orm";
+import {
+ sendDiscordNotification,
+ sendEmailNotification,
+ sendSlackNotification,
+ sendTelegramNotification,
+} from "./utils";
+
+interface Props {
+ projectName: string;
+ applicationName: string;
+ applicationType: string;
+ buildLink: string;
+}
+
+export const sendBuildSuccessNotifications = async ({
+ projectName,
+ applicationName,
+ applicationType,
+ buildLink,
+}: Props) => {
+ const date = new Date();
+ const notificationList = await db.query.notifications.findMany({
+ where: eq(notifications.appDeploy, true),
+ with: {
+ email: true,
+ discord: true,
+ telegram: true,
+ slack: true,
+ },
+ });
+
+ for (const notification of notificationList) {
+ const { email, discord, telegram, slack } = notification;
+
+ if (email) {
+ await sendEmailNotification(
+ email,
+ "Build success for dokploy",
+ render(
+ BuildSuccessEmail({
+ projectName,
+ applicationName,
+ applicationType,
+ buildLink,
+ date: date.toLocaleString(),
+ }),
+ ),
+ );
+ }
+
+ if (discord) {
+ await sendDiscordNotification(discord, {
+ title: "✅ Build Success",
+ color: 0x00ff00,
+ fields: [
+ {
+ name: "Project",
+ value: projectName,
+ inline: true,
+ },
+ {
+ name: "Application",
+ value: applicationName,
+ inline: true,
+ },
+ {
+ name: "Type",
+ value: applicationType,
+ inline: true,
+ },
+ {
+ name: "Build Link",
+ value: buildLink,
+ },
+ ],
+ timestamp: date.toISOString(),
+ footer: {
+ text: "Dokploy Build Notification",
+ },
+ });
+ }
+
+ if (telegram) {
+ await sendTelegramNotification(
+ telegram,
+ `
+ ✅ Build Success
+
+ Project: ${projectName}
+ Application: ${applicationName}
+ Type: ${applicationType}
+ Time: ${date.toLocaleString()}
+
+ Build Details: ${buildLink}
+ `,
+ );
+ }
+
+ if (slack) {
+ const { webhookUrl, channel } = slack;
+ await sendSlackNotification(slack, {
+ channel: channel,
+ attachments: [
+ {
+ color: "#00FF00",
+ pretext: ":white_check_mark: *Build Success*",
+ fields: [
+ {
+ title: "Project",
+ value: projectName,
+ short: true,
+ },
+ {
+ title: "Application",
+ value: applicationName,
+ short: true,
+ },
+ {
+ title: "Type",
+ value: applicationType,
+ short: true,
+ },
+ {
+ title: "Time",
+ value: date.toLocaleString(),
+ short: true,
+ },
+ {
+ title: "Build Link",
+ value: buildLink,
+ },
+ ],
+ actions: [
+ {
+ type: "button",
+ text: "View Build Details",
+ url: "https://doks.dev/build-details",
+ },
+ ],
+ },
+ ],
+ });
+ }
+ }
+};
diff --git a/server/utils/notifications/database-backup.ts b/server/utils/notifications/database-backup.ts
new file mode 100644
index 000000000..9555799ff
--- /dev/null
+++ b/server/utils/notifications/database-backup.ts
@@ -0,0 +1,183 @@
+import DatabaseBackupEmail from "@/emails/emails/database-backup";
+import { db } from "@/server/db";
+import { notifications } from "@/server/db/schema";
+import { render } from "@react-email/components";
+import { eq } from "drizzle-orm";
+import {
+ sendDiscordNotification,
+ sendEmailNotification,
+ sendSlackNotification,
+ sendTelegramNotification,
+} from "./utils";
+
+export const sendDatabaseBackupNotifications = async ({
+ projectName,
+ applicationName,
+ databaseType,
+ type,
+ errorMessage,
+}: {
+ projectName: string;
+ applicationName: string;
+ databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
+ type: "error" | "success";
+ errorMessage?: string;
+}) => {
+ const date = new Date();
+ const notificationList = await db.query.notifications.findMany({
+ where: eq(notifications.databaseBackup, true),
+ with: {
+ email: true,
+ discord: true,
+ telegram: true,
+ slack: true,
+ },
+ });
+
+ for (const notification of notificationList) {
+ const { email, discord, telegram, slack } = notification;
+
+ if (email) {
+ await sendEmailNotification(
+ email,
+ "Database backup for dokploy",
+ render(
+ DatabaseBackupEmail({
+ projectName,
+ applicationName,
+ databaseType,
+ type,
+ errorMessage,
+ date: date.toLocaleString(),
+ }),
+ ),
+ );
+ }
+
+ if (discord) {
+ await sendDiscordNotification(discord, {
+ title:
+ type === "success"
+ ? "✅ Database Backup Successful"
+ : "❌ Database Backup Failed",
+ color: type === "success" ? 0x00ff00 : 0xff0000,
+ fields: [
+ {
+ name: "Project",
+ value: projectName,
+ inline: true,
+ },
+ {
+ name: "Application",
+ value: applicationName,
+ inline: true,
+ },
+ {
+ name: "Type",
+ value: databaseType,
+ inline: true,
+ },
+ {
+ name: "Time",
+ value: date.toLocaleString(),
+ inline: true,
+ },
+ {
+ name: "Type",
+ value: type,
+ },
+ ...(type === "error" && errorMessage
+ ? [
+ {
+ name: "Error Message",
+ value: errorMessage,
+ },
+ ]
+ : []),
+ ],
+ timestamp: date.toISOString(),
+ footer: {
+ text: "Dokploy Database Backup Notification",
+ },
+ });
+ }
+
+ if (telegram) {
+ const statusEmoji = type === "success" ? "✅" : "❌";
+ const messageText = `
+ ${statusEmoji} Database Backup ${type === "success" ? "Successful" : "Failed"}
+
+ Project: ${projectName}
+ Application: ${applicationName}
+ Type: ${databaseType}
+ Time: ${date.toLocaleString()}
+
+ Status: ${type === "success" ? "Successful" : "Failed"}
+ ${type === "error" && errorMessage ? `Error: ${errorMessage}` : ""}
+ `;
+ 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: *Database Backup Successful*"
+ : ":x: *Database 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: "Type",
+ value: databaseType,
+ short: true,
+ },
+ {
+ title: "Time",
+ value: date.toLocaleString(),
+ short: true,
+ },
+ {
+ title: "Type",
+ value: type,
+ },
+ {
+ title: "Status",
+ value: type === "success" ? "Successful" : "Failed",
+ },
+ ],
+ actions: [
+ {
+ type: "button",
+ text: "View Build Details",
+ url: "https://doks.dev/build-details",
+ },
+ ],
+ },
+ ],
+ });
+ }
+ }
+};
diff --git a/server/utils/notifications/docker-cleanup.ts b/server/utils/notifications/docker-cleanup.ts
new file mode 100644
index 000000000..c2d39a0e8
--- /dev/null
+++ b/server/utils/notifications/docker-cleanup.ts
@@ -0,0 +1,97 @@
+import DockerCleanupEmail from "@/emails/emails/docker-cleanup";
+import { db } from "@/server/db";
+import { notifications } from "@/server/db/schema";
+import { render } from "@react-email/components";
+import { eq } from "drizzle-orm";
+import {
+ sendDiscordNotification,
+ sendEmailNotification,
+ sendSlackNotification,
+ sendTelegramNotification,
+} from "./utils";
+
+export const sendDockerCleanupNotifications = async (
+ message = "Docker cleanup for dokploy",
+) => {
+ const date = new Date();
+ const notificationList = await db.query.notifications.findMany({
+ where: eq(notifications.dockerCleanup, true),
+ with: {
+ email: true,
+ discord: true,
+ telegram: true,
+ slack: true,
+ },
+ });
+
+ for (const notification of notificationList) {
+ const { email, discord, telegram, slack } = notification;
+
+ if (email) {
+ await sendEmailNotification(
+ email,
+ "Docker cleanup for dokploy",
+ render(DockerCleanupEmail({ message, date: date.toLocaleString() })),
+ );
+ }
+
+ if (discord) {
+ await sendDiscordNotification(discord, {
+ title: "✅ Docker Cleanup",
+ color: 0x00ff00,
+ fields: [
+ {
+ name: "Message",
+ value: message,
+ },
+ ],
+ timestamp: date.toISOString(),
+ footer: {
+ text: "Dokploy Docker Cleanup Notification",
+ },
+ });
+ }
+
+ if (telegram) {
+ await sendTelegramNotification(
+ telegram,
+ `
+ ✅ Docker Cleanup
+ Message: ${message}
+ Time: ${date.toLocaleString()}
+ `,
+ );
+ }
+
+ if (slack) {
+ const { channel } = slack;
+ await sendSlackNotification(slack, {
+ channel: channel,
+ attachments: [
+ {
+ color: "#00FF00",
+ pretext: ":white_check_mark: *Docker Cleanup*",
+ fields: [
+ {
+ title: "Message",
+ value: message,
+ },
+ {
+ title: "Time",
+ value: date.toLocaleString(),
+ short: true,
+ },
+ ],
+ actions: [
+ {
+ type: "button",
+ text: "View Build Details",
+ url: "https://doks.dev/build-details",
+ },
+ ],
+ },
+ ],
+ });
+ }
+ }
+};
diff --git a/server/utils/notifications/dokploy-restart.ts b/server/utils/notifications/dokploy-restart.ts
new file mode 100644
index 000000000..6535b4467
--- /dev/null
+++ b/server/utils/notifications/dokploy-restart.ts
@@ -0,0 +1,91 @@
+import DokployRestartEmail from "@/emails/emails/dokploy-restart";
+import { db } from "@/server/db";
+import { notifications } from "@/server/db/schema";
+import { render } from "@react-email/components";
+import { eq } from "drizzle-orm";
+import {
+ sendDiscordNotification,
+ sendEmailNotification,
+ sendSlackNotification,
+ sendTelegramNotification,
+} from "./utils";
+
+export const sendDokployRestartNotifications = async () => {
+ const date = new Date();
+ const notificationList = await db.query.notifications.findMany({
+ where: eq(notifications.dokployRestart, true),
+ with: {
+ email: true,
+ discord: true,
+ telegram: true,
+ slack: true,
+ },
+ });
+
+ for (const notification of notificationList) {
+ const { email, discord, telegram, slack } = notification;
+
+ if (email) {
+ await sendEmailNotification(
+ email,
+ "Dokploy Server Restarted",
+ render(DokployRestartEmail({ date: date.toLocaleString() })),
+ );
+ }
+
+ if (discord) {
+ await sendDiscordNotification(discord, {
+ title: "✅ Dokploy Server Restarted",
+ color: 0x00ff00,
+ fields: [
+ {
+ name: "Time",
+ value: date.toLocaleString(),
+ inline: true,
+ },
+ ],
+ timestamp: date.toISOString(),
+ footer: {
+ text: "Dokploy Restart Notification",
+ },
+ });
+ }
+
+ if (telegram) {
+ await sendTelegramNotification(
+ telegram,
+ `
+ ✅ Dokploy Serverd Restarted
+ Time: ${date.toLocaleString()}
+ `,
+ );
+ }
+
+ if (slack) {
+ const { channel } = slack;
+ await sendSlackNotification(slack, {
+ channel: channel,
+ attachments: [
+ {
+ color: "#FF0000",
+ pretext: ":white_check_mark: *Dokploy Server Restarted*",
+ fields: [
+ {
+ title: "Time",
+ value: date.toLocaleString(),
+ short: true,
+ },
+ ],
+ actions: [
+ {
+ type: "button",
+ text: "View Build Details",
+ url: "https://doks.dev/build-details",
+ },
+ ],
+ },
+ ],
+ });
+ }
+ }
+};
diff --git a/server/utils/notifications/utils.ts b/server/utils/notifications/utils.ts
new file mode 100644
index 000000000..3b1513bfe
--- /dev/null
+++ b/server/utils/notifications/utils.ts
@@ -0,0 +1,69 @@
+import type { discord, email, slack, telegram } from "@/server/db/schema";
+import nodemailer from "nodemailer";
+
+export const sendEmailNotification = async (
+ connection: typeof email.$inferInsert,
+ subject: string,
+ htmlContent: string,
+) => {
+ const { smtpServer, smtpPort, username, password, fromAddress, toAddresses } =
+ connection;
+ const transporter = nodemailer.createTransport({
+ host: smtpServer,
+ port: smtpPort,
+ secure: smtpPort === 465,
+ auth: { user: username, pass: password },
+ });
+
+ await transporter.sendMail({
+ from: fromAddress,
+ to: toAddresses.join(", "),
+ subject,
+ html: htmlContent,
+ });
+};
+
+export const sendDiscordNotification = async (
+ connection: typeof discord.$inferInsert,
+ embed: any,
+) => {
+ 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");
+};
+
+export const sendTelegramNotification = async (
+ connection: typeof telegram.$inferInsert,
+ messageText: string,
+) => {
+ const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
+ const response = await fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ chat_id: connection.chatId,
+ text: messageText,
+ parse_mode: "HTML",
+ disable_web_page_preview: true,
+ }),
+ });
+
+ if (!response.ok) throw new Error("Failed to send Telegram notification");
+};
+
+export const sendSlackNotification = async (
+ connection: typeof slack.$inferInsert,
+ message: any,
+) => {
+ 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");
+};