mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(notifications): add emails and methos for each action
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
import { findMariadbByBackupId } from "../services/mariadb";
|
||||
import { findMongoByBackupId } from "../services/mongo";
|
||||
import { findMySqlByBackupId } from "../services/mysql";
|
||||
import { sendDatabaseBackupNotifications } from "../services/notification";
|
||||
import { findPostgresByBackupId } from "../services/postgres";
|
||||
|
||||
export const backupRouter = createTRPCRouter({
|
||||
@@ -90,6 +91,7 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const postgres = await findPostgresByBackupId(backup.backupId);
|
||||
await runPostgresBackup(postgres, backup);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -35,6 +35,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||
import { appRouter } from "../root";
|
||||
import { findAdmin, updateAdmin } from "../services/admin";
|
||||
import { sendDockerCleanupNotifications } from "../services/notification";
|
||||
import {
|
||||
getDokployImage,
|
||||
getDokployVersion,
|
||||
@@ -86,6 +87,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
|
||||
return true;
|
||||
}),
|
||||
cleanMonitoring: adminProcedure.mutation(async () => {
|
||||
@@ -144,6 +146,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
await sendDockerCleanupNotifications();
|
||||
});
|
||||
} else {
|
||||
const currentJob = scheduledJobs["docker-cleanup"];
|
||||
|
||||
@@ -17,7 +17,10 @@ import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { findAdmin } from "./admin";
|
||||
import { createDeployment, updateDeploymentStatus } from "./deployment";
|
||||
import { sendBuildErrorNotifications } from "./notification";
|
||||
import {
|
||||
sendBuildErrorNotifications,
|
||||
sendBuildSuccessNotifications,
|
||||
} from "./notification";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
@@ -157,13 +160,20 @@ export const deployApplication = async ({
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.project.name,
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
buildLink: deployment.logPath,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error on build", error);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
await sendBuildErrorNotifications({
|
||||
projectName: application.project.name,
|
||||
applicationName: application.appName,
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
errorMessage: error?.message || "Error to build",
|
||||
buildLink: deployment.logPath,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
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,
|
||||
@@ -20,10 +25,9 @@ import {
|
||||
} 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 { and, eq, isNotNull } from "drizzle-orm";
|
||||
import type SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
import { BuildFailedEmail } from "@/emails/emails/build-failed";
|
||||
|
||||
export type Notification = typeof notifications.$inferSelect;
|
||||
|
||||
@@ -53,10 +57,10 @@ export const createSlackNotification = async (
|
||||
slackId: newSlack.slackId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "slack",
|
||||
})
|
||||
.returning()
|
||||
@@ -82,10 +86,10 @@ export const updateSlackNotification = async (
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
@@ -138,10 +142,10 @@ export const createTelegramNotification = async (
|
||||
telegramId: newTelegram.telegramId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "telegram",
|
||||
})
|
||||
.returning()
|
||||
@@ -167,10 +171,10 @@ export const updateTelegramNotification = async (
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
@@ -222,10 +226,10 @@ export const createDiscordNotification = async (
|
||||
discordId: newDiscord.discordId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "discord",
|
||||
})
|
||||
.returning()
|
||||
@@ -251,10 +255,10 @@ export const updateDiscordNotification = async (
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
@@ -310,10 +314,10 @@ export const createEmailNotification = async (
|
||||
emailId: newEmail.emailId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "email",
|
||||
})
|
||||
.returning()
|
||||
@@ -339,10 +343,10 @@ export const updateEmailNotification = async (
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
@@ -507,69 +511,13 @@ export const sendEmailTestNotification = async (
|
||||
console.log("Email notification sent successfully");
|
||||
};
|
||||
|
||||
export const sendBuildFailedEmail = async ({
|
||||
projectName,
|
||||
applicationName,
|
||||
applicationType,
|
||||
errorMessage,
|
||||
buildLink,
|
||||
}: {
|
||||
interface BuildFailedEmailProps {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
applicationType: string;
|
||||
errorMessage: string;
|
||||
buildLink: string;
|
||||
}) => {
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: and(
|
||||
isNotNull(notifications.emailId),
|
||||
eq(notifications.appBuildError, true),
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email } = notification;
|
||||
if (email) {
|
||||
const {
|
||||
smtpServer,
|
||||
smtpPort,
|
||||
username,
|
||||
password,
|
||||
fromAddress,
|
||||
toAddresses,
|
||||
} = email;
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpServer,
|
||||
port: smtpPort,
|
||||
secure: smtpPort === 465,
|
||||
auth: {
|
||||
user: username,
|
||||
pass: password,
|
||||
},
|
||||
} as SMTPTransport.Options);
|
||||
const mailOptions = {
|
||||
from: fromAddress,
|
||||
to: toAddresses?.join(", "),
|
||||
subject: "Build failed for dokploy",
|
||||
html: render(
|
||||
BuildFailedEmail({
|
||||
projectName,
|
||||
applicationName,
|
||||
applicationType,
|
||||
errorMessage,
|
||||
buildLink,
|
||||
}),
|
||||
),
|
||||
};
|
||||
await transporter.sendMail(mailOptions);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// export const
|
||||
}
|
||||
|
||||
export const sendBuildErrorNotifications = async ({
|
||||
projectName,
|
||||
@@ -577,13 +525,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
applicationType,
|
||||
errorMessage,
|
||||
buildLink,
|
||||
}: {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
applicationType: string;
|
||||
errorMessage: string;
|
||||
buildLink: string;
|
||||
}) => {
|
||||
}: BuildFailedEmailProps) => {
|
||||
const date = new Date();
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.appBuildError, true),
|
||||
@@ -626,6 +568,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
applicationType,
|
||||
errorMessage,
|
||||
buildLink,
|
||||
date: date.toLocaleString(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
@@ -636,7 +579,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
const { webhookUrl } = discord;
|
||||
const embed = {
|
||||
title: "⚠️ Build Failed",
|
||||
color: 0xff0000, // Rojo
|
||||
color: 0xff0000,
|
||||
fields: [
|
||||
{
|
||||
name: "Project",
|
||||
@@ -775,3 +718,791 @@ export const sendBuildErrorNotifications = async ({
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
const {
|
||||
smtpServer,
|
||||
smtpPort,
|
||||
username,
|
||||
password,
|
||||
fromAddress,
|
||||
toAddresses,
|
||||
} = email;
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpServer,
|
||||
port: smtpPort,
|
||||
secure: smtpPort === 465,
|
||||
auth: {
|
||||
user: username,
|
||||
pass: password,
|
||||
},
|
||||
} as SMTPTransport.Options);
|
||||
const mailOptions = {
|
||||
from: fromAddress,
|
||||
to: toAddresses?.join(", "),
|
||||
subject: "Build success for dokploy",
|
||||
html: render(
|
||||
BuildSuccessEmail({
|
||||
projectName,
|
||||
applicationName,
|
||||
applicationType,
|
||||
buildLink,
|
||||
date: date.toLocaleString(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
await transporter.sendMail(mailOptions);
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const { webhookUrl } = discord;
|
||||
const embed = {
|
||||
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",
|
||||
},
|
||||
};
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
embeds: [embed],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const { botToken, chatId } = telegram;
|
||||
const messageText = `
|
||||
<b>✅ Build Success</b>
|
||||
|
||||
<b>Project:</b> ${projectName}
|
||||
<b>Application:</b> ${applicationName}
|
||||
<b>Type:</b> ${applicationType}
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
|
||||
<b>Build Details:</b> ${buildLink}
|
||||
`;
|
||||
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text: messageText,
|
||||
parse_mode: "HTML",
|
||||
disable_web_page_preview: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { webhookUrl, channel } = slack;
|
||||
const message = {
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
const {
|
||||
smtpServer,
|
||||
smtpPort,
|
||||
username,
|
||||
password,
|
||||
fromAddress,
|
||||
toAddresses,
|
||||
} = email;
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpServer,
|
||||
port: smtpPort,
|
||||
secure: smtpPort === 465,
|
||||
auth: {
|
||||
user: username,
|
||||
pass: password,
|
||||
},
|
||||
} as SMTPTransport.Options);
|
||||
const mailOptions = {
|
||||
from: fromAddress,
|
||||
to: toAddresses?.join(", "),
|
||||
subject: "Database backup for dokploy",
|
||||
html: render(
|
||||
DatabaseBackupEmail({
|
||||
projectName,
|
||||
applicationName,
|
||||
databaseType,
|
||||
type,
|
||||
errorMessage,
|
||||
date: date.toLocaleString(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
await transporter.sendMail(mailOptions);
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const { webhookUrl } = discord;
|
||||
const embed = {
|
||||
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,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Database Backup Notification",
|
||||
},
|
||||
};
|
||||
|
||||
if (type === "error" && errorMessage) {
|
||||
embed.fields.push({
|
||||
name: "Error Message",
|
||||
value: errorMessage as unknown as string,
|
||||
});
|
||||
}
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
embeds: [embed],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const { botToken, chatId } = telegram;
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const messageText = `
|
||||
<b>${statusEmoji} Database Backup ${type === "success" ? "Successful" : "Failed"}</b>
|
||||
|
||||
<b>Project:</b> ${projectName}
|
||||
<b>Application:</b> ${applicationName}
|
||||
<b>Type:</b> ${databaseType}
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
|
||||
<b>Status:</b> ${type === "success" ? "Successful" : "Failed"}
|
||||
${type === "error" && errorMessage ? `<b>Error:</b> ${errorMessage}` : ""}
|
||||
`;
|
||||
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text: messageText,
|
||||
parse_mode: "HTML",
|
||||
disable_web_page_preview: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { webhookUrl, channel } = slack;
|
||||
const message = {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||
pretext:
|
||||
type === "success"
|
||||
? ":white_check_mark: *Database Backup Successful*"
|
||||
: ":x: *Database Backup Failed*",
|
||||
fields: [
|
||||
{
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (type === "error" && errorMessage) {
|
||||
message.attachments[0].fields.push({
|
||||
title: "Error Message",
|
||||
value: errorMessage,
|
||||
});
|
||||
}
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
const {
|
||||
smtpServer,
|
||||
smtpPort,
|
||||
username,
|
||||
password,
|
||||
fromAddress,
|
||||
toAddresses,
|
||||
} = email;
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpServer,
|
||||
port: smtpPort,
|
||||
secure: smtpPort === 465,
|
||||
auth: {
|
||||
user: username,
|
||||
pass: password,
|
||||
},
|
||||
} as SMTPTransport.Options);
|
||||
const mailOptions = {
|
||||
from: fromAddress,
|
||||
to: toAddresses?.join(", "),
|
||||
subject: "Docker cleanup for dokploy",
|
||||
html: render(
|
||||
DockerCleanupEmail({
|
||||
message,
|
||||
date: date.toLocaleString(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
await transporter.sendMail(mailOptions);
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const { webhookUrl } = discord;
|
||||
const embed = {
|
||||
title: "✅ Docker Cleanup",
|
||||
color: 0x00ff00,
|
||||
fields: [
|
||||
{
|
||||
name: "Message",
|
||||
value: message,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Docker Cleanup Notification",
|
||||
},
|
||||
};
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
embeds: [embed],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const { botToken, chatId } = telegram;
|
||||
const messageText = `
|
||||
<b>✅ Docker Cleanup</b>
|
||||
<b>Message:</b> ${message}
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
`;
|
||||
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text: messageText,
|
||||
parse_mode: "HTML",
|
||||
disable_web_page_preview: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { webhookUrl, channel } = slack;
|
||||
const messageResponse = {
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(messageResponse),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const sendEmailNotification = async (
|
||||
connection: typeof email.$inferSelect,
|
||||
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.$inferSelect,
|
||||
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.$inferSelect,
|
||||
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.$inferSelect,
|
||||
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) {
|
||||
const {
|
||||
smtpServer,
|
||||
smtpPort,
|
||||
username,
|
||||
password,
|
||||
fromAddress,
|
||||
toAddresses,
|
||||
} = email;
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpServer,
|
||||
port: smtpPort,
|
||||
secure: smtpPort === 465,
|
||||
auth: {
|
||||
user: username,
|
||||
pass: password,
|
||||
},
|
||||
} as SMTPTransport.Options);
|
||||
const mailOptions = {
|
||||
from: fromAddress,
|
||||
to: toAddresses?.join(", "),
|
||||
subject: "Dokploy Server Restarted",
|
||||
html: render(
|
||||
DokployRestartEmail({
|
||||
date: date.toLocaleString(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
await transporter.sendMail(mailOptions);
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const { webhookUrl } = discord;
|
||||
const embed = {
|
||||
title: "✅ Dokploy Server Restarted",
|
||||
color: 0xff0000,
|
||||
fields: [
|
||||
{
|
||||
name: "Time",
|
||||
value: date.toLocaleString(),
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Restart Notification",
|
||||
},
|
||||
};
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
embeds: [embed],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const { botToken, chatId } = telegram;
|
||||
const messageText = `
|
||||
<b>✅ Dokploy Serverd Restarted</b>
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
`;
|
||||
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text: messageText,
|
||||
parse_mode: "HTML",
|
||||
disable_web_page_preview: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { webhookUrl, channel } = slack;
|
||||
const message = {
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error to send test notification");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
export const notificationType = pgEnum("notificationType", [
|
||||
"slack",
|
||||
@@ -18,10 +18,10 @@ export const notifications = pgTable("notification", {
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appDeploy: boolean("appDeploy").notNull().default(false),
|
||||
userJoin: boolean("userJoin").notNull().default(false),
|
||||
appBuildError: boolean("appBuildError").notNull().default(false),
|
||||
databaseBackup: boolean("databaseBackup").notNull().default(false),
|
||||
dokployRestart: boolean("dokployRestart").notNull().default(false),
|
||||
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
|
||||
notificationType: notificationType("notificationType").notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -107,7 +107,7 @@ export const apiCreateSlack = notificationsSchema
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
userJoin: true,
|
||||
dockerCleanup: true,
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().min(1),
|
||||
@@ -132,7 +132,7 @@ export const apiCreateTelegram = notificationsSchema
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
userJoin: true,
|
||||
dockerCleanup: true,
|
||||
})
|
||||
.extend({
|
||||
botToken: z.string().min(1),
|
||||
@@ -157,7 +157,7 @@ export const apiCreateDiscord = notificationsSchema
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
userJoin: true,
|
||||
dockerCleanup: true,
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().min(1),
|
||||
@@ -180,7 +180,7 @@ export const apiCreateEmail = notificationsSchema
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
userJoin: true,
|
||||
dockerCleanup: true,
|
||||
})
|
||||
.extend({
|
||||
smtpServer: z.string().min(1),
|
||||
|
||||
@@ -2,6 +2,7 @@ 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";
|
||||
@@ -57,6 +58,8 @@ void app.prepare().then(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 7000));
|
||||
await migration();
|
||||
}
|
||||
await sendDokployRestartNotifications();
|
||||
|
||||
server.listen(PORT);
|
||||
console.log("Server Started:", PORT);
|
||||
deploymentWorker.run();
|
||||
|
||||
@@ -2,6 +2,8 @@ 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 { execAsync } from "../process/execAsync";
|
||||
import { uploadToS3 } from "./utils";
|
||||
@@ -10,7 +12,8 @@ export const runMariadbBackup = async (
|
||||
mariadb: Mariadb,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { appName, databasePassword, databaseUser } = mariadb;
|
||||
const { appName, databasePassword, databaseUser, projectId, name } = mariadb;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
@@ -31,8 +34,22 @@ export const runMariadbBackup = async (
|
||||
`docker cp ${containerId}:/backup/${backupFileName} ${hostPath}`,
|
||||
);
|
||||
await uploadToS3(destination, bucketDestination, hostPath);
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mariadb",
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mariadb",
|
||||
type: "error",
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
await unlink(hostPath);
|
||||
|
||||
@@ -2,13 +2,16 @@ 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 { execAsync } from "../process/execAsync";
|
||||
import { uploadToS3 } from "./utils";
|
||||
|
||||
// mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true
|
||||
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
const { appName, databasePassword, databaseUser } = mongo;
|
||||
const { appName, databasePassword, databaseUser, projectId, name } = mongo;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.dump.gz`;
|
||||
@@ -27,8 +30,22 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
);
|
||||
await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`);
|
||||
await uploadToS3(destination, bucketDestination, hostPath);
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mongodb",
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mongodb",
|
||||
type: "error",
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
await unlink(hostPath);
|
||||
|
||||
@@ -2,12 +2,15 @@ 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 { execAsync } from "../process/execAsync";
|
||||
import { uploadToS3 } from "./utils";
|
||||
|
||||
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
const { appName, databaseRootPassword } = mysql;
|
||||
const { appName, databaseRootPassword, projectId, name } = mysql;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
@@ -29,8 +32,21 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
`docker cp ${containerId}:/backup/${backupFileName} ${hostPath}`,
|
||||
);
|
||||
await uploadToS3(destination, bucketDestination, hostPath);
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mysql",
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mysql",
|
||||
type: "error",
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
await unlink(hostPath);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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 { execAsync } from "../process/execAsync";
|
||||
import { uploadToS3 } from "./utils";
|
||||
@@ -10,7 +12,9 @@ export const runPostgresBackup = async (
|
||||
postgres: Postgres,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { appName, databaseUser } = postgres;
|
||||
const { appName, databaseUser, name, projectId } = postgres;
|
||||
const project = await findProjectById(projectId);
|
||||
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
@@ -29,8 +33,21 @@ export const runPostgresBackup = async (
|
||||
await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`);
|
||||
|
||||
await uploadToS3(destination, bucketDestination, hostPath);
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "postgres",
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "postgres",
|
||||
type: "error",
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
});
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
await unlink(hostPath);
|
||||
|
||||
Reference in New Issue
Block a user