Merge branch 'feature/add-custom-webhook-notification-provider' of github.com:ChristoferMendes/dokploy into feature/add-custom-webhook-notification-provider

This commit is contained in:
ChristoferMendes
2025-09-29 08:54:21 -03:00
11 changed files with 2322 additions and 2313 deletions
@@ -46,10 +46,13 @@ const convertPairsToJson = (pairs: KeyValuePair[]): string => {
return ""; return "";
} }
const obj = enabledPairs.reduce((acc, pair) => { const obj = enabledPairs.reduce(
(acc, pair) => {
acc[pair.key.trim()] = pair.value; acc[pair.key.trim()] = pair.value;
return acc; return acc;
}, {} as Record<string, string>); },
{} as Record<string, string>,
);
return JSON.stringify(obj, null, 2); return JSON.stringify(obj, null, 2);
}; };
@@ -84,10 +87,10 @@ export const KeyValueInput = ({
const updatePair = ( const updatePair = (
index: number, index: number,
field: keyof KeyValuePair, field: keyof KeyValuePair,
newValue: string | boolean newValue: string | boolean,
) => { ) => {
const updatedPairs = pairs.map((pair, i) => const updatedPairs = pairs.map((pair, i) =>
i === index ? { ...pair, [field]: newValue } : pair i === index ? { ...pair, [field]: newValue } : pair,
); );
syncPairsWithParent(updatedPairs); syncPairsWithParent(updatedPairs);
}; };
@@ -123,13 +123,13 @@ export const ShowNotifications = () => {
}) })
.then(() => { .then(() => {
toast.success( toast.success(
"Notification deleted successfully" "Notification deleted successfully",
); );
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error( toast.error(
"Error deleting notification" "Error deleting notification",
); );
}); });
}} }}
+13 -13
View File
@@ -70,7 +70,7 @@ export const notificationRouter = createTRPCRouter({
try { try {
return await createSlackNotification( return await createSlackNotification(
input, input,
ctx.session.activeOrganizationId ctx.session.activeOrganizationId,
); );
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -122,7 +122,7 @@ export const notificationRouter = createTRPCRouter({
try { try {
return await createTelegramNotification( return await createTelegramNotification(
input, input,
ctx.session.activeOrganizationId ctx.session.activeOrganizationId,
); );
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -176,7 +176,7 @@ export const notificationRouter = createTRPCRouter({
try { try {
return await createDiscordNotification( return await createDiscordNotification(
input, input,
ctx.session.activeOrganizationId ctx.session.activeOrganizationId,
); );
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -239,7 +239,7 @@ export const notificationRouter = createTRPCRouter({
try { try {
return await createEmailNotification( return await createEmailNotification(
input, input,
ctx.session.activeOrganizationId ctx.session.activeOrganizationId,
); );
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -279,7 +279,7 @@ export const notificationRouter = createTRPCRouter({
await sendEmailNotification( await sendEmailNotification(
input, input,
"Test Email", "Test Email",
"<p>Hi, From Dokploy 👋</p>" "<p>Hi, From Dokploy 👋</p>",
); );
return true; return true;
} catch (error) { } catch (error) {
@@ -350,7 +350,7 @@ export const notificationRouter = createTRPCRouter({
Message: z.string(), Message: z.string(),
Timestamp: z.string(), Timestamp: z.string(),
Token: z.string(), Token: z.string(),
}) }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -361,7 +361,7 @@ export const notificationRouter = createTRPCRouter({
.select() .select()
.from(users_temp) .from(users_temp)
.where( .where(
sql`${users_temp.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}` sql`${users_temp.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
); );
if (!result?.[0]?.id) { if (!result?.[0]?.id) {
@@ -378,7 +378,7 @@ export const notificationRouter = createTRPCRouter({
.select() .select()
.from(server) .from(server)
.where( .where(
sql`${server.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}` sql`${server.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
); );
if (!result?.[0]?.organizationId) { if (!result?.[0]?.organizationId) {
@@ -410,7 +410,7 @@ export const notificationRouter = createTRPCRouter({
try { try {
return await createGotifyNotification( return await createGotifyNotification(
input, input,
ctx.session.activeOrganizationId ctx.session.activeOrganizationId,
); );
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -449,7 +449,7 @@ export const notificationRouter = createTRPCRouter({
await sendGotifyNotification( await sendGotifyNotification(
input, input,
"Test Notification", "Test Notification",
"Hi, From Dokploy 👋" "Hi, From Dokploy 👋",
); );
return true; return true;
} catch (error) { } catch (error) {
@@ -466,7 +466,7 @@ export const notificationRouter = createTRPCRouter({
try { try {
return await createNtfyNotification( return await createNtfyNotification(
input, input,
ctx.session.activeOrganizationId ctx.session.activeOrganizationId,
); );
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -507,7 +507,7 @@ export const notificationRouter = createTRPCRouter({
"Test Notification", "Test Notification",
"", "",
"view, visit Dokploy on Github, https://github.com/dokploy/dokploy, clear=true;", "view, visit Dokploy on Github, https://github.com/dokploy/dokploy, clear=true;",
"Hi, From Dokploy 👋" "Hi, From Dokploy 👋",
); );
return true; return true;
} catch (error) { } catch (error) {
@@ -524,7 +524,7 @@ export const notificationRouter = createTRPCRouter({
try { try {
return await createCustomNotification( return await createCustomNotification(
input, input,
ctx.session.activeOrganizationId ctx.session.activeOrganizationId,
); );
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
+15 -15
View File
@@ -30,7 +30,7 @@ export type Notification = typeof notifications.$inferSelect;
export const createSlackNotification = async ( export const createSlackNotification = async (
input: typeof apiCreateSlack._type, input: typeof apiCreateSlack._type,
organizationId: string organizationId: string,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newSlack = await tx const newSlack = await tx
@@ -78,7 +78,7 @@ export const createSlackNotification = async (
}; };
export const updateSlackNotification = async ( export const updateSlackNotification = async (
input: typeof apiUpdateSlack._type input: typeof apiUpdateSlack._type,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newDestination = await tx const newDestination = await tx
@@ -120,7 +120,7 @@ export const updateSlackNotification = async (
export const createTelegramNotification = async ( export const createTelegramNotification = async (
input: typeof apiCreateTelegram._type, input: typeof apiCreateTelegram._type,
organizationId: string organizationId: string,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newTelegram = await tx const newTelegram = await tx
@@ -169,7 +169,7 @@ export const createTelegramNotification = async (
}; };
export const updateTelegramNotification = async ( export const updateTelegramNotification = async (
input: typeof apiUpdateTelegram._type input: typeof apiUpdateTelegram._type,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newDestination = await tx const newDestination = await tx
@@ -212,7 +212,7 @@ export const updateTelegramNotification = async (
export const createDiscordNotification = async ( export const createDiscordNotification = async (
input: typeof apiCreateDiscord._type, input: typeof apiCreateDiscord._type,
organizationId: string organizationId: string,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newDiscord = await tx const newDiscord = await tx
@@ -260,7 +260,7 @@ export const createDiscordNotification = async (
}; };
export const updateDiscordNotification = async ( export const updateDiscordNotification = async (
input: typeof apiUpdateDiscord._type input: typeof apiUpdateDiscord._type,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newDestination = await tx const newDestination = await tx
@@ -302,7 +302,7 @@ export const updateDiscordNotification = async (
export const createEmailNotification = async ( export const createEmailNotification = async (
input: typeof apiCreateEmail._type, input: typeof apiCreateEmail._type,
organizationId: string organizationId: string,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newEmail = await tx const newEmail = await tx
@@ -354,7 +354,7 @@ export const createEmailNotification = async (
}; };
export const updateEmailNotification = async ( export const updateEmailNotification = async (
input: typeof apiUpdateEmail._type input: typeof apiUpdateEmail._type,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newDestination = await tx const newDestination = await tx
@@ -400,7 +400,7 @@ export const updateEmailNotification = async (
export const createGotifyNotification = async ( export const createGotifyNotification = async (
input: typeof apiCreateGotify._type, input: typeof apiCreateGotify._type,
organizationId: string organizationId: string,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newGotify = await tx const newGotify = await tx
@@ -449,7 +449,7 @@ export const createGotifyNotification = async (
}; };
export const updateGotifyNotification = async ( export const updateGotifyNotification = async (
input: typeof apiUpdateGotify._type input: typeof apiUpdateGotify._type,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newDestination = await tx const newDestination = await tx
@@ -490,7 +490,7 @@ export const updateGotifyNotification = async (
export const createNtfyNotification = async ( export const createNtfyNotification = async (
input: typeof apiCreateNtfy._type, input: typeof apiCreateNtfy._type,
organizationId: string organizationId: string,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newNtfy = await tx const newNtfy = await tx
@@ -539,7 +539,7 @@ export const createNtfyNotification = async (
}; };
export const updateNtfyNotification = async ( export const updateNtfyNotification = async (
input: typeof apiUpdateNtfy._type input: typeof apiUpdateNtfy._type,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newDestination = await tx const newDestination = await tx
@@ -580,7 +580,7 @@ export const updateNtfyNotification = async (
export const createCustomNotification = async ( export const createCustomNotification = async (
input: typeof apiCreateCustom._type, input: typeof apiCreateCustom._type,
organizationId: string organizationId: string,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newCustom = await tx const newCustom = await tx
@@ -628,7 +628,7 @@ export const createCustomNotification = async (
}; };
export const updateCustomNotification = async ( export const updateCustomNotification = async (
input: typeof apiUpdateCustom._type input: typeof apiUpdateCustom._type,
) => { ) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const newDestination = await tx const newDestination = await tx
@@ -699,7 +699,7 @@ export const removeNotificationById = async (notificationId: string) => {
export const updateNotificationById = async ( export const updateNotificationById = async (
notificationId: string, notificationId: string,
notificationData: Partial<Notification> notificationData: Partial<Notification>,
) => { ) => {
const result = await db const result = await db
.update(notifications) .update(notifications)
@@ -36,7 +36,7 @@ export const sendBuildErrorNotifications = async ({
const notificationList = await db.query.notifications.findMany({ const notificationList = await db.query.notifications.findMany({
where: and( where: and(
eq(notifications.appBuildError, true), eq(notifications.appBuildError, true),
eq(notifications.organizationId, organizationId) eq(notifications.organizationId, organizationId),
), ),
with: { with: {
email: true, email: true,
@@ -61,7 +61,7 @@ export const sendBuildErrorNotifications = async ({
errorMessage: errorMessage, errorMessage: errorMessage,
buildLink, buildLink,
date: date.toLocaleString(), date: date.toLocaleString(),
}) }),
).catch(); ).catch();
await sendEmailNotification(email, "Build failed for dokploy", template); await sendEmailNotification(email, "Build failed for dokploy", template);
} }
@@ -133,7 +133,7 @@ export const sendBuildErrorNotifications = async ({
`${decorate("❔", `Type: ${applicationType}`)}` + `${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("⚠️", `Error:\n${errorMessage}`)}` + `${decorate("⚠️", `Error:\n${errorMessage}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}` `${decorate("🔗", `Build details:\n${buildLink}`)}`,
); );
} }
@@ -147,7 +147,7 @@ export const sendBuildErrorNotifications = async ({
`⚙️Application: ${applicationName}\n` + `⚙️Application: ${applicationName}\n` +
`❔Type: ${applicationType}\n` + `❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}\n` + `🕒Date: ${date.toLocaleString()}\n` +
`⚠️Error:\n${errorMessage}` `⚠️Error:\n${errorMessage}`,
); );
} }
@@ -165,12 +165,12 @@ export const sendBuildErrorNotifications = async ({
telegram, telegram,
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format( `<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(
date, date,
"PP" "PP",
)}\n<b>Time:</b> ${format( )}\n<b>Time:</b> ${format(
date, date,
"pp" "pp",
)}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`, )}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
inlineButton inlineButton,
); );
} }
@@ -37,7 +37,7 @@ export const sendBuildSuccessNotifications = async ({
const notificationList = await db.query.notifications.findMany({ const notificationList = await db.query.notifications.findMany({
where: and( where: and(
eq(notifications.appDeploy, true), eq(notifications.appDeploy, true),
eq(notifications.organizationId, organizationId) eq(notifications.organizationId, organizationId),
), ),
with: { with: {
email: true, email: true,
@@ -62,7 +62,7 @@ export const sendBuildSuccessNotifications = async ({
applicationType, applicationType,
buildLink, buildLink,
date: date.toLocaleString(), date: date.toLocaleString(),
}) }),
).catch(); ).catch();
await sendEmailNotification(email, "Build success for dokploy", template); await sendEmailNotification(email, "Build success for dokploy", template);
} }
@@ -127,7 +127,7 @@ export const sendBuildSuccessNotifications = async ({
`${decorate("⚙️", `Application: ${applicationName}`)}` + `${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` + `${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}` `${decorate("🔗", `Build details:\n${buildLink}`)}`,
); );
} }
@@ -140,14 +140,14 @@ export const sendBuildSuccessNotifications = async ({
`🛠Project: ${projectName}\n` + `🛠Project: ${projectName}\n` +
`⚙️Application: ${applicationName}\n` + `⚙️Application: ${applicationName}\n` +
`❔Type: ${applicationType}\n` + `❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}` `🕒Date: ${date.toLocaleString()}`,
); );
} }
if (telegram) { if (telegram) {
const chunkArray = <T>(array: T[], chunkSize: number): T[][] => const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
array.slice(i * chunkSize, i * chunkSize + chunkSize) array.slice(i * chunkSize, i * chunkSize + chunkSize),
); );
const inlineButton = [ const inlineButton = [
@@ -161,7 +161,7 @@ export const sendBuildSuccessNotifications = async ({
chunk.map((data) => ({ chunk.map((data) => ({
text: data.host, text: data.host,
url: `${data.https ? "https" : "http"}://${data.host}`, url: `${data.https ? "https" : "http"}://${data.host}`,
})) })),
), ),
]; ];
@@ -169,9 +169,9 @@ export const sendBuildSuccessNotifications = async ({
telegram, telegram,
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format( `<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(
date, date,
"PP" "PP",
)}\n<b>Time:</b> ${format(date, "pp")}`, )}\n<b>Time:</b> ${format(date, "pp")}`,
inlineButton inlineButton,
); );
} }
@@ -50,7 +50,8 @@ export const sendDatabaseBackupNotifications = async ({
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } = notification; const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification;
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
@@ -244,7 +245,10 @@ export const sendDatabaseBackupNotifications = async ({
if (custom) { if (custom) {
await sendCustomNotification(custom, { await sendCustomNotification(custom, {
title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`, title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`,
message: type === "success" ? "Database backup completed successfully" : "Database backup failed", message:
type === "success"
? "Database backup completed successfully"
: "Database backup failed",
projectName, projectName,
applicationName, applicationName,
databaseType, databaseType,
@@ -37,7 +37,8 @@ export const sendDockerCleanupNotifications = async (
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } = notification; const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification;
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
@@ -31,7 +31,8 @@ export const sendDokployRestartNotifications = async () => {
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } = notification; const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification;
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
@@ -12,7 +12,7 @@ import nodemailer from "nodemailer";
export const sendEmailNotification = async ( export const sendEmailNotification = async (
connection: typeof email.$inferInsert, connection: typeof email.$inferInsert,
subject: string, subject: string,
htmlContent: string htmlContent: string,
) => { ) => {
try { try {
const { const {
@@ -42,7 +42,7 @@ export const sendEmailNotification = async (
export const sendDiscordNotification = async ( export const sendDiscordNotification = async (
connection: typeof discord.$inferInsert, connection: typeof discord.$inferInsert,
embed: any embed: any,
) => { ) => {
// try { // try {
await fetch(connection.webhookUrl, { await fetch(connection.webhookUrl, {
@@ -61,7 +61,7 @@ export const sendTelegramNotification = async (
inlineButton?: { inlineButton?: {
text: string; text: string;
url: string; url: string;
}[][] }[][],
) => { ) => {
try { try {
const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`; const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
@@ -86,7 +86,7 @@ export const sendTelegramNotification = async (
export const sendSlackNotification = async ( export const sendSlackNotification = async (
connection: typeof slack.$inferInsert, connection: typeof slack.$inferInsert,
message: any message: any,
) => { ) => {
try { try {
await fetch(connection.webhookUrl, { await fetch(connection.webhookUrl, {
@@ -102,7 +102,7 @@ export const sendSlackNotification = async (
export const sendGotifyNotification = async ( export const sendGotifyNotification = async (
connection: typeof gotify.$inferInsert, connection: typeof gotify.$inferInsert,
title: string, title: string,
message: string message: string,
) => { ) => {
const response = await fetch(`${connection.serverUrl}/message`, { const response = await fetch(`${connection.serverUrl}/message`, {
method: "POST", method: "POST",
@@ -124,7 +124,7 @@ export const sendGotifyNotification = async (
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
`Failed to send Gotify notification: ${response.statusText}` `Failed to send Gotify notification: ${response.statusText}`,
); );
} }
}; };
@@ -134,7 +134,7 @@ export const sendNtfyNotification = async (
title: string, title: string,
tags: string, tags: string,
actions: string, actions: string,
message: string message: string,
) => { ) => {
const response = await fetch(`${connection.serverUrl}/${connection.topic}`, { const response = await fetch(`${connection.serverUrl}/${connection.topic}`, {
method: "POST", method: "POST",
@@ -155,7 +155,7 @@ export const sendNtfyNotification = async (
export const sendCustomNotification = async ( export const sendCustomNotification = async (
connection: typeof custom.$inferInsert, connection: typeof custom.$inferInsert,
payload: Record<string, any> payload: Record<string, any>,
) => { ) => {
try { try {
// Parse headers if provided // Parse headers if provided
@@ -181,7 +181,7 @@ export const sendCustomNotification = async (
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
`Failed to send custom notification: ${response.statusText}` `Failed to send custom notification: ${response.statusText}`,
); );
} }