Files
dokploy/packages/server/src/utils/notifications/utils.ts
Mauricio Siu 9687ed0d83 feat: add invoice notification settings and email notifications for payments
- Introduced a new feature allowing users to enable or disable invoice email notifications in the billing settings.
- Implemented email notifications for successful invoice payments and payment failures, enhancing user communication regarding billing.
- Updated the database schema to include a new column for storing user preferences on invoice notifications.
- Added corresponding email templates for invoice notifications and payment failure alerts.

These changes improve user experience by keeping users informed about their billing status and actions required.
2026-04-11 00:18:23 -06:00

394 lines
8.8 KiB
TypeScript

import type {
custom,
discord,
email,
gotify,
lark,
mattermost,
ntfy,
pushover,
resend,
slack,
teams,
telegram,
} from "@dokploy/server/db/schema";
import nodemailer from "nodemailer";
import { Resend } from "resend";
export const sendEmailNotification = async (
connection: typeof email.$inferInsert,
subject: string,
htmlContent: string,
attachments?: { filename: string; content: Buffer }[],
) => {
try {
const {
smtpServer,
smtpPort,
username,
password,
fromAddress,
toAddresses,
} = connection;
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
auth: { user: username, pass: password },
});
await transporter.sendMail({
from: fromAddress,
to: toAddresses.join(", "),
subject,
html: htmlContent,
textEncoding: "base64",
attachments,
});
} catch (err) {
console.log(err);
throw new Error(
`Failed to send email notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
export const sendResendNotification = async (
connection: typeof resend.$inferInsert,
subject: string,
htmlContent: string,
) => {
try {
const client = new Resend(connection.apiKey);
const result = await client.emails.send({
from: connection.fromAddress,
to: connection.toAddresses,
subject,
html: htmlContent,
});
if (result.error) {
throw new Error(result.error.message);
}
} catch (err) {
console.log(err);
throw new Error(
`Failed to send Resend notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
export const sendDiscordNotification = async (
connection: typeof discord.$inferInsert,
embed: any,
) => {
try {
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 ${response.statusText}`,
);
}
} catch (err) {
console.log("error", err);
throw new Error(
`Failed to send discord notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
export const sendTelegramNotification = async (
connection: typeof telegram.$inferInsert,
messageText: string,
inlineButton?: {
text: string;
url: string;
}[][],
) => {
try {
const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: connection.chatId,
message_thread_id: connection.messageThreadId,
text: messageText,
parse_mode: "HTML",
disable_web_page_preview: true,
reply_markup: {
inline_keyboard: inlineButton,
},
}),
});
} catch (err) {
console.log(err);
}
};
export const sendSlackNotification = async (
connection: typeof slack.$inferInsert,
message: any,
) => {
try {
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 ${response.statusText}`,
);
}
} catch (err) {
console.log("error", err);
throw new Error(
`Failed to send slack notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
export const sendGotifyNotification = async (
connection: typeof gotify.$inferInsert,
title: string,
message: string,
) => {
const response = await fetch(`${connection.serverUrl}/message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Gotify-Key": connection.appToken,
},
body: JSON.stringify({
title: title,
message: message,
priority: connection.priority,
extras: {
"client::display": {
contentType: "text/plain",
},
},
}),
});
if (!response.ok) {
throw new Error(
`Failed to send Gotify notification: ${response.statusText}`,
);
}
};
export const sendNtfyNotification = async (
connection: typeof ntfy.$inferInsert,
title: string,
tags: string,
actions: string,
message: string,
) => {
const response = await fetch(`${connection.serverUrl}/${connection.topic}`, {
method: "POST",
headers: {
...(connection.accessToken && {
Authorization: `Bearer ${connection.accessToken}`,
}),
"X-Priority": connection.priority?.toString() || "3",
"X-Title": title,
"X-Tags": tags,
"X-Actions": actions,
},
body: message,
});
if (!response.ok) {
throw new Error(`Failed to send ntfy notification: ${response.statusText}`);
}
};
export const sendMattermostNotification = async (
connection: typeof mattermost.$inferInsert,
message: any,
) => {
const payload = {
...message,
// Only include username if it's provided and not empty
...(message.username?.trim() && { username: message.username }),
// Only include channel if it's provided and not empty
...(message.channel?.trim() && {
channel: `#${message.channel.replace("#", "")}`,
}),
};
const response = await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(
`Failed to send Mattermost notification: ${response.statusText}`,
);
}
};
export const sendCustomNotification = async (
connection: typeof custom.$inferInsert,
payload: Record<string, any>,
) => {
try {
// Merge default headers with custom headers (now already an object from jsonb)
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(connection.headers || {}),
};
// Default body with payload
const body = JSON.stringify(payload);
const response = await fetch(connection.endpoint, {
method: "POST",
headers,
body,
});
if (!response.ok) {
throw new Error(
`Failed to send custom notification: ${response.statusText}`,
);
}
return response;
} catch (error) {
console.error("Error sending custom notification:", error);
throw error;
}
};
export const sendLarkNotification = async (
connection: typeof lark.$inferInsert,
message: any,
) => {
try {
await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message),
});
} catch (err) {
console.log(err);
}
};
export interface TeamsAdaptiveCardMessage {
title: string;
themeColor?: string;
facts?: { name: string; value: string }[];
potentialAction?: { type: "Action.OpenUrl"; title: string; url: string };
}
export const sendTeamsNotification = async (
connection: typeof teams.$inferInsert,
message: TeamsAdaptiveCardMessage,
) => {
try {
const bodyElements: Record<string, unknown>[] = [
{
type: "TextBlock",
text: message.title,
size: "Medium",
weight: "Bolder",
wrap: true,
},
];
if (message.facts && message.facts.length > 0) {
bodyElements.push({
type: "FactSet",
facts: message.facts.map((f) => ({
title: f.name,
value: f.value,
})),
});
}
const cardContent: Record<string, unknown> = {
type: "AdaptiveCard",
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
version: "1.4",
body: bodyElements,
};
if (message.potentialAction) {
cardContent.actions = [
{
type: "Action.OpenUrl",
title: message.potentialAction.title,
url: message.potentialAction.url,
},
];
}
const payload = {
type: "message",
attachments: [
{
contentType: "application/vnd.microsoft.card.adaptive",
content: cardContent,
},
],
};
const response = await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(
`Failed to send Teams notification: ${response.statusText}`,
);
}
} catch (err) {
console.log(err);
throw new Error(
`Failed to send Teams notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
export const sendPushoverNotification = async (
connection: typeof pushover.$inferInsert,
title: string,
message: string,
) => {
const formData = new URLSearchParams();
formData.append("token", connection.apiToken);
formData.append("user", connection.userKey);
formData.append("title", title);
formData.append("message", message);
formData.append("priority", connection.priority?.toString() || "0");
// For emergency priority (2), retry and expire are required
if (connection.priority === 2) {
formData.append("retry", connection.retry?.toString() || "30");
formData.append("expire", connection.expire?.toString() || "3600");
}
const response = await fetch("https://api.pushover.net/1/messages.json", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(
`Failed to send Pushover notification: ${response.statusText}`,
);
}
};