feat(notifications): add build failed and invitation emails from react-email

This commit is contained in:
Mauricio Siu
2024-07-14 02:49:21 -06:00
parent 5fadd73732
commit 79ad0818f5
38 changed files with 15799 additions and 353 deletions

View File

@@ -10,11 +10,17 @@ import {
apiCreateSlack,
apiCreateTelegram,
apiFindOneNotification,
apiSendTest,
apiUpdateDestination,
apiTestDiscordConnection,
apiTestEmailConnection,
apiTestSlackConnection,
apiTestTelegramConnection,
apiUpdateDiscord,
apiUpdateEmail,
apiUpdateSlack,
apiUpdateTelegram,
notifications,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { updateDestinationById } from "../services/destination";
import {
createDiscordNotification,
createEmailNotification,
@@ -22,8 +28,16 @@ import {
createTelegramNotification,
findNotificationById,
removeNotificationById,
sendDiscordTestNotification,
sendEmailTestNotification,
sendSlackTestNotification,
sendTelegramTestNotification,
updateDiscordNotification,
updateEmailNotification,
updateSlackNotification,
updateTelegramNotification,
} from "../services/notification";
import nodemailer from "nodemailer";
import { desc } from "drizzle-orm";
export const notificationRouter = createTRPCRouter({
createSlack: adminProcedure
@@ -35,7 +49,34 @@ export const notificationRouter = createTRPCRouter({
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the destination",
message: "Error to create the notification",
cause: error,
});
}
}),
updateSlack: adminProcedure
.input(apiUpdateSlack)
.mutation(async ({ input }) => {
try {
return await updateSlackNotification(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update the notification",
cause: error,
});
}
}),
testSlackConnection: adminProcedure
.input(apiTestSlackConnection)
.mutation(async ({ input }) => {
try {
await sendSlackTestNotification(input);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to test the notification",
cause: error,
});
}
@@ -48,7 +89,35 @@ export const notificationRouter = createTRPCRouter({
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the destination",
message: "Error to create the notification",
cause: error,
});
}
}),
updateTelegram: adminProcedure
.input(apiUpdateTelegram)
.mutation(async ({ input }) => {
try {
return await updateTelegramNotification(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update the notification",
cause: error,
});
}
}),
testTelegramConnection: adminProcedure
.input(apiTestTelegramConnection)
.mutation(async ({ input }) => {
try {
await sendTelegramTestNotification(input);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to test the notification",
cause: error,
});
}
@@ -61,7 +130,36 @@ export const notificationRouter = createTRPCRouter({
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the destination",
message: "Error to create the notification",
cause: error,
});
}
}),
updateDiscord: adminProcedure
.input(apiUpdateDiscord)
.mutation(async ({ input }) => {
try {
return await updateDiscordNotification(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update the notification",
cause: error,
});
}
}),
testDiscordConnection: adminProcedure
.input(apiTestDiscordConnection)
.mutation(async ({ input }) => {
try {
await sendDiscordTestNotification(input);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to test the notification",
cause: error,
});
}
@@ -72,9 +170,37 @@ export const notificationRouter = createTRPCRouter({
try {
return await createEmailNotification(input);
} catch (error) {
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the destination",
message: "Error to create the notification",
cause: error,
});
}
}),
updateEmail: adminProcedure
.input(apiUpdateEmail)
.mutation(async ({ input }) => {
try {
return await updateEmailNotification(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update the notification",
cause: error,
});
}
}),
testEmailConnection: adminProcedure
.input(apiTestEmailConnection)
.mutation(async ({ input }) => {
try {
await sendEmailTestNotification(input);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to test the notification",
cause: error,
});
}
@@ -97,127 +223,6 @@ export const notificationRouter = createTRPCRouter({
const notification = await findNotificationById(input.notificationId);
return notification;
}),
testConnection: adminProcedure
.input(apiSendTest)
.mutation(async ({ input }) => {
const notificationType = input.notificationType;
console.log(input);
if (notificationType === "slack") {
// go to your slack dashboard
// go to integrations
// add a new integration
// select incoming webhook
// copy the webhook url
console.log("test slack");
const { webhookUrl, channel } = input;
try {
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text: "Test notification", channel }),
});
} catch (err) {
console.log(err);
}
} else if (notificationType === "telegram") {
// start telegram
// search BotFather
// send /newbot
// name
// name-with-bot-at-the-end
// copy the token
// search @userinfobot
// send /start
// copy the Id
const { botToken, chatId } = input;
try {
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: "Test notification",
}),
});
if (!response.ok) {
throw new Error(
`Error sending Telegram notification: ${response.statusText}`,
);
}
console.log("Telegram notification sent successfully");
} catch (error) {
console.error("Error sending Telegram notification:", error);
throw new Error("Error sending Telegram notification");
}
} else if (notificationType === "discord") {
const { webhookUrl } = input;
try {
// go to your discord server
// go to settings
// go to integrations
// add a new integration
// select webhook
// copy the webhook url
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: "Test notification",
}),
});
if (!response.ok) {
throw new Error(
`Error sending Discord notification: ${response.statusText}`,
);
}
console.log("Discord notification sent successfully");
} catch (error) {
console.error("Error sending Discord notification:", error);
throw new Error("Error sending Discord notification");
}
} else if (notificationType === "email") {
const { smtpServer, smtpPort, username, password, toAddresses } = input;
try {
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
secure: smtpPort === "465",
auth: {
user: username,
pass: password,
},
});
// need to add a valid from address
const fromAddress = "no-reply@emails.dokploy.com";
const mailOptions = {
from: fromAddress,
to: toAddresses?.join(", "),
subject: "Test email",
text: "Test email",
};
await transporter.sendMail(mailOptions);
console.log("Email notification sent successfully");
} catch (error) {
console.error("Error sending Email notification:", error);
throw new Error("Error sending Email notification");
}
}
}),
all: adminProcedure.query(async () => {
return await db.query.notifications.findMany({
with: {
@@ -226,19 +231,7 @@ export const notificationRouter = createTRPCRouter({
discord: true,
email: true,
},
orderBy: desc(notifications.createdAt),
});
}),
update: adminProcedure
.input(apiUpdateDestination)
.mutation(async ({ input }) => {
try {
return await updateDestinationById(input.destinationId, input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update this destination",
cause: error,
});
}
}),
});

View File

@@ -18,6 +18,7 @@ import { getAdvancedStats } from "@/server/monitoring/utilts";
import { validUniqueServerAppName } from "./project";
import { generatePassword } from "@/templates/utils";
import { generateAppName } from "@/server/db/schema/utils";
import { sendBuildFailedEmail } from "./notification";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
@@ -157,8 +158,17 @@ export const deployApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done");
} catch (error) {
console.log("Error on build", error);
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
await sendBuildFailedEmail({
projectName: application.project.name,
applicationName: application.appName,
applicationType: "application",
errorMessage: error?.message || "Error to build",
buildLink: deployment.logPath,
});
console.log(
"Error on ",
application.buildType,

View File

@@ -4,14 +4,26 @@ import {
type apiCreateEmail,
type apiCreateSlack,
type apiCreateTelegram,
type apiTestDiscordConnection,
type apiTestEmailConnection,
type apiTestSlackConnection,
type apiTestTelegramConnection,
type apiUpdateDiscord,
type apiUpdateEmail,
type apiUpdateSlack,
type apiUpdateTelegram,
discord,
email,
notifications,
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 { 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;
@@ -61,6 +73,45 @@ export const createSlackNotification = async (
});
};
export const updateSlackNotification = async (
input: typeof apiUpdateSlack._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(slack)
.set({
channel: input.channel,
webhookUrl: input.webhookUrl,
})
.where(eq(slack.slackId, input.slackId))
.returning()
.then((value) => value[0]);
return newDestination;
});
};
export const createTelegramNotification = async (
input: typeof apiCreateTelegram._type,
) => {
@@ -107,6 +158,45 @@ export const createTelegramNotification = async (
});
};
export const updateTelegramNotification = async (
input: typeof apiUpdateTelegram._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(telegram)
.set({
botToken: input.botToken,
chatId: input.chatId,
})
.where(eq(telegram.telegramId, input.telegramId))
.returning()
.then((value) => value[0]);
return newDestination;
});
};
export const createDiscordNotification = async (
input: typeof apiCreateDiscord._type,
) => {
@@ -152,6 +242,44 @@ export const createDiscordNotification = async (
});
};
export const updateDiscordNotification = async (
input: typeof apiUpdateDiscord._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(discord)
.set({
webhookUrl: input.webhookUrl,
})
.where(eq(discord.discordId, input.discordId))
.returning()
.then((value) => value[0]);
return newDestination;
});
};
export const createEmailNotification = async (
input: typeof apiCreateEmail._type,
) => {
@@ -163,6 +291,7 @@ export const createEmailNotification = async (
smtpPort: input.smtpPort,
username: input.username,
password: input.password,
fromAddress: input.fromAddress,
toAddresses: input.toAddresses,
})
.returning()
@@ -201,6 +330,49 @@ export const createEmailNotification = async (
});
};
export const updateEmailNotification = async (
input: typeof apiUpdateEmail._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(email)
.set({
smtpServer: input.smtpServer,
smtpPort: input.smtpPort,
username: input.username,
password: input.password,
fromAddress: input.fromAddress,
toAddresses: input.toAddresses,
})
.where(eq(email.emailId, input.emailId))
.returning()
.then((value) => value[0]);
return newDestination;
});
};
export const findNotificationById = async (notificationId: string) => {
const notification = await db.query.notifications.findFirst({
where: eq(notifications.notificationId, notificationId),
@@ -244,21 +416,187 @@ export const updateDestinationById = async (
return result[0];
};
export const sendNotification = async (
notificationData: Partial<Notification>,
export const sendSlackTestNotification = async (
slackTestConnection: typeof apiTestSlackConnection._type,
) => {
// if(notificationData.notificationType === "slack"){
// const { webhookUrl, channel } = notificationData;
// try {
// const response = await fetch(webhookUrl, {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify({ text: "Test notification", channel }),
// });
// } catch (err) {
// console.log(err);
// }
// }
const { webhookUrl, channel } = slackTestConnection;
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text: "Hi, From Dokploy 👋", channel }),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
};
export const sendTelegramTestNotification = async (
telegramTestConnection: typeof apiTestTelegramConnection._type,
) => {
const { botToken, chatId } = telegramTestConnection;
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: "Hi, From Dokploy 👋",
}),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
};
export const sendDiscordTestNotification = async (
discordTestConnection: typeof apiTestDiscordConnection._type,
) => {
const { webhookUrl } = discordTestConnection;
// go to your discord server
// go to settings
// go to integrations
// add a new integration
// select webhook
// copy the webhook url
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: "Hi, From Dokploy 👋",
}),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
};
export const sendEmailTestNotification = async (
emailTestConnection: typeof apiTestEmailConnection._type,
) => {
const { smtpServer, smtpPort, username, password, toAddresses, fromAddress } =
emailTestConnection;
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
secure: smtpPort === 465,
auth: {
user: username,
pass: password,
},
} as SMTPTransport.Options);
// need to add a valid from address
const mailOptions = {
from: fromAddress,
to: toAddresses?.join(", "),
subject: "Test email",
text: "Hi, From Dokploy 👋",
};
await transporter.sendMail(mailOptions);
console.log("Email notification sent successfully");
};
// export const sendInvitationEmail = async (
// emailTestConnection: typeof apiTestEmailConnection._type,
// inviteLink: string,
// toEmail: string,
// ) => {
// const { smtpServer, smtpPort, username, password, fromAddress } =
// emailTestConnection;
// const transporter = nodemailer.createTransport({
// host: smtpServer,
// port: smtpPort,
// secure: smtpPort === 465,
// auth: {
// user: username,
// pass: password,
// },
// } as SMTPTransport.Options);
// // need to add a valid from address
// const mailOptions = {
// from: fromAddress,
// to: toEmail,
// subject: "Invitation to join Dokploy",
// html: InvitationTemplate({
// inviteLink: inviteLink,
// toEmail: toEmail,
// }),
// };
// await transporter.sendMail(mailOptions);
// console.log("Email notification sent successfully");
// };
export const sendBuildFailedEmail = async ({
projectName,
applicationName,
applicationType,
errorMessage,
buildLink,
}: {
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);
}
}
};