refactor(notifications): change render to renderAsync in emails

This commit is contained in:
Mauricio Siu
2024-07-19 23:56:48 -06:00
parent 18f892096b
commit 44ee326057
11 changed files with 147 additions and 126 deletions

View File

@@ -20,6 +20,12 @@ import {
apiUpdateTelegram, apiUpdateTelegram,
notifications, notifications,
} from "@/server/db/schema"; } from "@/server/db/schema";
import {
sendDiscordNotification,
sendEmailNotification,
sendSlackNotification,
sendTelegramNotification,
} from "@/server/utils/notifications/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { desc } from "drizzle-orm"; import { desc } from "drizzle-orm";
import { import {
@@ -29,10 +35,6 @@ import {
createTelegramNotification, createTelegramNotification,
findNotificationById, findNotificationById,
removeNotificationById, removeNotificationById,
sendDiscordNotification,
sendEmailNotification,
sendSlackNotification,
sendTelegramNotification,
updateDiscordNotification, updateDiscordNotification,
updateEmailNotification, updateEmailNotification,
updateSlackNotification, updateSlackNotification,

View File

@@ -146,3 +146,12 @@ export const removeUserByAuthId = async (authId: string) => {
.returning() .returning()
.then((res) => res[0]); .then((res) => res[0]);
}; };
export const getDokployUrl = async () => {
const admin = await findAdmin();
if (admin.host) {
return `https://${admin.host}`;
}
return `http://${admin.serverIp}:${process.env.PORT}`;
};

View File

@@ -15,7 +15,7 @@ import { createTraefikConfig } from "@/server/utils/traefik/application";
import { generatePassword } from "@/templates/utils"; import { generatePassword } from "@/templates/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { findAdmin } from "./admin"; import { findAdmin, getDokployUrl } from "./admin";
import { createDeployment, updateDeploymentStatus } from "./deployment"; import { createDeployment, updateDeploymentStatus } from "./deployment";
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error"; import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
@@ -140,6 +140,7 @@ export const deployApplication = async ({
descriptionLog: string; descriptionLog: string;
}) => { }) => {
const application = await findApplicationById(applicationId); const application = await findApplicationById(applicationId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
const admin = await findAdmin(); const admin = await findAdmin();
const deployment = await createDeployment({ const deployment = await createDeployment({
applicationId: applicationId, applicationId: applicationId,
@@ -164,7 +165,7 @@ export const deployApplication = async ({
projectName: application.project.name, projectName: application.project.name,
applicationName: application.name, applicationName: application.name,
applicationType: "application", applicationType: "application",
buildLink: deployment.logPath, buildLink,
}); });
} catch (error) { } catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
@@ -173,8 +174,9 @@ export const deployApplication = async ({
projectName: application.project.name, projectName: application.project.name,
applicationName: application.name, applicationName: application.name,
applicationType: "application", applicationType: "application",
// @ts-ignore
errorMessage: error?.message || "Error to build", errorMessage: error?.message || "Error to build",
buildLink: deployment.logPath, buildLink,
}); });
console.log( console.log(

View File

@@ -5,6 +5,8 @@ import { type apiCreateCompose, compose } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema/utils"; import { generateAppName } from "@/server/db/schema/utils";
import { buildCompose } from "@/server/utils/builders/compose"; import { buildCompose } from "@/server/utils/builders/compose";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@/server/utils/docker/types";
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
import { execAsync } from "@/server/utils/process/execAsync"; import { execAsync } from "@/server/utils/process/execAsync";
import { cloneGitRepository } from "@/server/utils/providers/git"; import { cloneGitRepository } from "@/server/utils/providers/git";
import { cloneGithubRepository } from "@/server/utils/providers/github"; import { cloneGithubRepository } from "@/server/utils/providers/github";
@@ -13,7 +15,7 @@ import { generatePassword } from "@/templates/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { findAdmin } from "./admin"; import { findAdmin, getDokployUrl } from "./admin";
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment"; import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
@@ -142,6 +144,7 @@ export const deployCompose = async ({
}) => { }) => {
const compose = await findComposeById(composeId); const compose = await findComposeById(composeId);
const admin = await findAdmin(); const admin = await findAdmin();
const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({ const deployment = await createDeploymentCompose({
composeId: composeId, composeId: composeId,
title: titleLog, title: titleLog,
@@ -161,11 +164,26 @@ export const deployCompose = async ({
await updateCompose(composeId, { await updateCompose(composeId, {
composeStatus: "done", composeStatus: "done",
}); });
await sendBuildSuccessNotifications({
projectName: compose.project.name,
applicationName: compose.name,
applicationType: "compose",
buildLink,
});
} catch (error) { } catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, { await updateCompose(composeId, {
composeStatus: "error", composeStatus: "error",
}); });
await sendBuildErrorNotifications({
projectName: compose.project.name,
applicationName: compose.name,
applicationType: "compose",
// @ts-ignore
errorMessage: error?.message || "Error to build",
buildLink,
});
throw error; throw error;
} }
}; };

View File

@@ -59,6 +59,7 @@ void app.prepare().then(async () => {
await migration(); await migration();
await sendDokployRestartNotifications(); await sendDokployRestartNotifications();
} }
server.listen(PORT); server.listen(PORT);
console.log("Server Started:", PORT); console.log("Server Started:", PORT);
deploymentWorker.run(); deploymentWorker.run();

View File

@@ -1,7 +1,7 @@
import BuildFailedEmail from "@/emails/emails/build-failed"; import BuildFailedEmail from "@/emails/emails/build-failed";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { notifications } from "@/server/db/schema"; import { notifications } from "@/server/db/schema";
import { render } from "@react-email/components"; import { renderAsync } from "@react-email/components";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { import {
sendDiscordNotification, sendDiscordNotification,
@@ -39,20 +39,17 @@ export const sendBuildErrorNotifications = async ({
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification; const { email, discord, telegram, slack } = notification;
if (email) { if (email) {
await sendEmailNotification( const template = await renderAsync(
email, BuildFailedEmail({
"Build failed for dokploy", projectName,
render( applicationName,
BuildFailedEmail({ applicationType,
projectName, errorMessage: errorMessage,
applicationName, buildLink,
applicationType, date: date.toLocaleString(),
errorMessage, }),
buildLink, ).catch();
date: date.toLocaleString(), await sendEmailNotification(email, "Build failed for dokploy", template);
}),
),
);
} }
if (discord) { if (discord) {
@@ -149,7 +146,7 @@ export const sendBuildErrorNotifications = async ({
{ {
type: "button", type: "button",
text: "View Build Details", text: "View Build Details",
url: "https://doks.dev/build-details", url: buildLink,
}, },
], ],
}, },

View File

@@ -1,7 +1,7 @@
import BuildSuccessEmail from "@/emails/emails/build-success"; import BuildSuccessEmail from "@/emails/emails/build-success";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { notifications } from "@/server/db/schema"; import { notifications } from "@/server/db/schema";
import { render } from "@react-email/components"; import { renderAsync } from "@react-email/components";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { import {
sendDiscordNotification, sendDiscordNotification,
@@ -38,19 +38,16 @@ export const sendBuildSuccessNotifications = async ({
const { email, discord, telegram, slack } = notification; const { email, discord, telegram, slack } = notification;
if (email) { if (email) {
await sendEmailNotification( const template = await renderAsync(
email, BuildSuccessEmail({
"Build success for dokploy", projectName,
render( applicationName,
BuildSuccessEmail({ applicationType,
projectName, buildLink,
applicationName, date: date.toLocaleString(),
applicationType, }),
buildLink, ).catch();
date: date.toLocaleString(), await sendEmailNotification(email, "Build success for dokploy", template);
}),
),
);
} }
if (discord) { if (discord) {
@@ -130,16 +127,12 @@ export const sendBuildSuccessNotifications = async ({
value: date.toLocaleString(), value: date.toLocaleString(),
short: true, short: true,
}, },
{
title: "Build Link",
value: buildLink,
},
], ],
actions: [ actions: [
{ {
type: "button", type: "button",
text: "View Build Details", text: "View Build Details",
url: "https://doks.dev/build-details", url: buildLink,
}, },
], ],
}, },

View File

@@ -1,7 +1,7 @@
import DatabaseBackupEmail from "@/emails/emails/database-backup"; import DatabaseBackupEmail from "@/emails/emails/database-backup";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { notifications } from "@/server/db/schema"; import { notifications } from "@/server/db/schema";
import { render } from "@react-email/components"; import { renderAsync } from "@react-email/components";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { import {
sendDiscordNotification, sendDiscordNotification,
@@ -38,19 +38,20 @@ export const sendDatabaseBackupNotifications = async ({
const { email, discord, telegram, slack } = notification; const { email, discord, telegram, slack } = notification;
if (email) { if (email) {
const template = await renderAsync(
DatabaseBackupEmail({
projectName,
applicationName,
databaseType,
type,
errorMessage,
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification( await sendEmailNotification(
email, email,
"Database backup for dokploy", "Database backup for dokploy",
render( template,
DatabaseBackupEmail({
projectName,
applicationName,
databaseType,
type,
errorMessage,
date: date.toLocaleString(),
}),
),
); );
} }
@@ -168,13 +169,6 @@ export const sendDatabaseBackupNotifications = async ({
value: type === "success" ? "Successful" : "Failed", value: type === "success" ? "Successful" : "Failed",
}, },
], ],
actions: [
{
type: "button",
text: "View Build Details",
url: "https://doks.dev/build-details",
},
],
}, },
], ],
}); });

View File

@@ -1,7 +1,7 @@
import DockerCleanupEmail from "@/emails/emails/docker-cleanup"; import DockerCleanupEmail from "@/emails/emails/docker-cleanup";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { notifications } from "@/server/db/schema"; import { notifications } from "@/server/db/schema";
import { render } from "@react-email/components"; import { renderAsync } from "@react-email/components";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { import {
sendDiscordNotification, sendDiscordNotification,
@@ -28,10 +28,14 @@ export const sendDockerCleanupNotifications = async (
const { email, discord, telegram, slack } = notification; const { email, discord, telegram, slack } = notification;
if (email) { if (email) {
const template = await renderAsync(
DockerCleanupEmail({ message, date: date.toLocaleString() }),
).catch();
await sendEmailNotification( await sendEmailNotification(
email, email,
"Docker cleanup for dokploy", "Docker cleanup for dokploy",
render(DockerCleanupEmail({ message, date: date.toLocaleString() })), template,
); );
} }
@@ -82,13 +86,6 @@ export const sendDockerCleanupNotifications = async (
short: true, short: true,
}, },
], ],
actions: [
{
type: "button",
text: "View Build Details",
url: "https://doks.dev/build-details",
},
],
}, },
], ],
}); });

View File

@@ -1,7 +1,7 @@
import DokployRestartEmail from "@/emails/emails/dokploy-restart"; import DokployRestartEmail from "@/emails/emails/dokploy-restart";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { notifications } from "@/server/db/schema"; import { notifications } from "@/server/db/schema";
import { render } from "@react-email/components"; import { renderAsync } from "@react-email/components";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { import {
sendDiscordNotification, sendDiscordNotification,
@@ -26,17 +26,16 @@ export const sendDokployRestartNotifications = async () => {
const { email, discord, telegram, slack } = notification; const { email, discord, telegram, slack } = notification;
if (email) { if (email) {
await sendEmailNotification( const template = await renderAsync(
email, DokployRestartEmail({ date: date.toLocaleString() }),
"Dokploy Server Restarted", ).catch();
render(DokployRestartEmail({ date: date.toLocaleString() })), await sendEmailNotification(email, "Dokploy Server Restarted", template);
);
} }
if (discord) { if (discord) {
await sendDiscordNotification(discord, { await sendDiscordNotification(discord, {
title: "✅ Dokploy Server Restarted", title: "✅ Dokploy Server Restarted",
color: 0x00ff00, color: 0xff0000,
fields: [ fields: [
{ {
name: "Time", name: "Time",
@@ -67,7 +66,7 @@ export const sendDokployRestartNotifications = async () => {
channel: channel, channel: channel,
attachments: [ attachments: [
{ {
color: "#FF0000", color: "#00FF00",
pretext: ":white_check_mark: *Dokploy Server Restarted*", pretext: ":white_check_mark: *Dokploy Server Restarted*",
fields: [ fields: [
{ {
@@ -76,13 +75,6 @@ export const sendDokployRestartNotifications = async () => {
short: true, short: true,
}, },
], ],
actions: [
{
type: "button",
text: "View Build Details",
url: "https://doks.dev/build-details",
},
],
}, },
], ],
}); });

View File

@@ -6,64 +6,80 @@ export const sendEmailNotification = async (
subject: string, subject: string,
htmlContent: string, htmlContent: string,
) => { ) => {
const { smtpServer, smtpPort, username, password, fromAddress, toAddresses } = try {
connection; const {
const transporter = nodemailer.createTransport({ smtpServer,
host: smtpServer, smtpPort,
port: smtpPort, username,
secure: smtpPort === 465, password,
auth: { user: username, pass: password }, fromAddress,
}); toAddresses,
} = connection;
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
secure: smtpPort === 465,
auth: { user: username, pass: password },
});
await transporter.sendMail({ await transporter.sendMail({
from: fromAddress, from: fromAddress,
to: toAddresses.join(", "), to: toAddresses.join(", "),
subject, subject,
html: htmlContent, html: htmlContent,
}); });
} catch (err) {
console.log(err);
}
}; };
export const sendDiscordNotification = async ( export const sendDiscordNotification = async (
connection: typeof discord.$inferInsert, connection: typeof discord.$inferInsert,
embed: any, embed: any,
) => { ) => {
const response = await fetch(connection.webhookUrl, { try {
method: "POST", await fetch(connection.webhookUrl, {
headers: { "Content-Type": "application/json" }, method: "POST",
body: JSON.stringify({ embeds: [embed] }), headers: { "Content-Type": "application/json" },
}); body: JSON.stringify({ embeds: [embed] }),
});
if (!response.ok) throw new Error("Failed to send Discord notification"); } catch (err) {
console.log(err);
}
}; };
export const sendTelegramNotification = async ( export const sendTelegramNotification = async (
connection: typeof telegram.$inferInsert, connection: typeof telegram.$inferInsert,
messageText: string, messageText: string,
) => { ) => {
const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`; try {
const response = await fetch(url, { const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
method: "POST", await fetch(url, {
headers: { "Content-Type": "application/json" }, method: "POST",
body: JSON.stringify({ headers: { "Content-Type": "application/json" },
chat_id: connection.chatId, body: JSON.stringify({
text: messageText, chat_id: connection.chatId,
parse_mode: "HTML", text: messageText,
disable_web_page_preview: true, parse_mode: "HTML",
}), disable_web_page_preview: true,
}); }),
});
if (!response.ok) throw new Error("Failed to send Telegram notification"); } catch (err) {
console.log(err);
}
}; };
export const sendSlackNotification = async ( export const sendSlackNotification = async (
connection: typeof slack.$inferInsert, connection: typeof slack.$inferInsert,
message: any, message: any,
) => { ) => {
const response = await fetch(connection.webhookUrl, { try {
method: "POST", await fetch(connection.webhookUrl, {
headers: { "Content-Type": "application/json" }, method: "POST",
body: JSON.stringify(message), headers: { "Content-Type": "application/json" },
}); body: JSON.stringify(message),
});
if (!response.ok) throw new Error("Failed to send Slack notification"); } catch (err) {
console.log(err);
}
}; };