diff --git a/server/api/services/application.ts b/server/api/services/application.ts index 67fb68c7..10040e55 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 cbbb7274..31ff91ea 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 0e8649cf..1f6b5c4f 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 518816bd..9277b225 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 eac8dc5c..1329b4f4 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 5f09a098..e662dbe1 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 a6371334..5c435a9b 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 00000000..53482365 --- /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 00000000..e8c8f106 --- /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 00000000..9555799f --- /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 00000000..c2d39a0e --- /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 00000000..6535b446 --- /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 00000000..3b1513bf --- /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"); +};