mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
- 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.
394 lines
8.8 KiB
TypeScript
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}`,
|
|
);
|
|
}
|
|
};
|