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
@@ -6,157 +6,160 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
interface KeyValuePair { interface KeyValuePair {
key: string; key: string;
value: string; value: string;
enabled: boolean; enabled: boolean;
} }
interface KeyValueInputProps { interface KeyValueInputProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
placeholder?: string; placeholder?: string;
label: string; label: string;
description?: string; description?: string;
} }
const createEmptyPair = (): KeyValuePair => ({ const createEmptyPair = (): KeyValuePair => ({
key: "", key: "",
value: "", value: "",
enabled: true, enabled: true,
}); });
const parseJsonToPairs = (jsonString: string): KeyValuePair[] => { const parseJsonToPairs = (jsonString: string): KeyValuePair[] => {
try { try {
const parsed = JSON.parse(jsonString); const parsed = JSON.parse(jsonString);
const pairs = Object.entries(parsed).map(([key, val]) => ({ const pairs = Object.entries(parsed).map(([key, val]) => ({
key, key,
value: String(val), value: String(val),
enabled: true, enabled: true,
})); }));
return pairs.length > 0 ? pairs : [createEmptyPair()]; return pairs.length > 0 ? pairs : [createEmptyPair()];
} catch { } catch {
return [createEmptyPair()]; return [createEmptyPair()];
} }
}; };
const convertPairsToJson = (pairs: KeyValuePair[]): string => { const convertPairsToJson = (pairs: KeyValuePair[]): string => {
const enabledPairs = pairs.filter((pair) => pair.enabled && pair.key.trim()); const enabledPairs = pairs.filter((pair) => pair.enabled && pair.key.trim());
if (enabledPairs.length === 0) { if (enabledPairs.length === 0) {
return ""; return "";
} }
const obj = enabledPairs.reduce((acc, pair) => { const obj = enabledPairs.reduce(
acc[pair.key.trim()] = pair.value; (acc, pair) => {
return acc; acc[pair.key.trim()] = pair.value;
}, {} as Record<string, string>); return acc;
},
{} as Record<string, string>,
);
return JSON.stringify(obj, null, 2); return JSON.stringify(obj, null, 2);
}; };
export const KeyValueInput = ({ export const KeyValueInput = ({
value, value,
onChange, onChange,
label, label,
description, description,
}: KeyValueInputProps) => { }: KeyValueInputProps) => {
const [pairs, setPairs] = useState<KeyValuePair[]>([]); const [pairs, setPairs] = useState<KeyValuePair[]>([]);
useEffect(() => { useEffect(() => {
const newPairs = value ? parseJsonToPairs(value) : [createEmptyPair()]; const newPairs = value ? parseJsonToPairs(value) : [createEmptyPair()];
setPairs(newPairs); setPairs(newPairs);
}, [value]); }, [value]);
const syncPairsWithParent = (newPairs: KeyValuePair[]) => { const syncPairsWithParent = (newPairs: KeyValuePair[]) => {
setPairs(newPairs); setPairs(newPairs);
onChange(convertPairsToJson(newPairs)); onChange(convertPairsToJson(newPairs));
}; };
const addPair = () => { const addPair = () => {
syncPairsWithParent([...pairs, createEmptyPair()]); syncPairsWithParent([...pairs, createEmptyPair()]);
}; };
const removePair = (index: number) => { const removePair = (index: number) => {
const filteredPairs = pairs.filter((_, i) => i !== index); const filteredPairs = pairs.filter((_, i) => i !== index);
syncPairsWithParent(filteredPairs); syncPairsWithParent(filteredPairs);
}; };
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);
}; };
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<Label className="text-sm font-medium">{label}</Label> <Label className="text-sm font-medium">{label}</Label>
{description && ( {description && (
<p className="text-sm text-muted-foreground mt-1">{description}</p> <p className="text-sm text-muted-foreground mt-1">{description}</p>
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{pairs.map((pair, index) => ( {pairs.map((pair, index) => (
<div <div
key={index} key={index}
className="flex items-center gap-2 p-2 border rounded-md bg-muted/50" className="flex items-center gap-2 p-2 border rounded-md bg-muted/50"
> >
<div className="flex items-center"> <div className="flex items-center">
<Checkbox <Checkbox
checked={pair.enabled} checked={pair.enabled}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
updatePair(index, "enabled", checked) updatePair(index, "enabled", checked)
} }
className="mr-2" className="mr-2"
/> />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<Input <Input
placeholder="Key" placeholder="Key"
value={pair.key} value={pair.key}
onChange={(e) => updatePair(index, "key", e.target.value)} onChange={(e) => updatePair(index, "key", e.target.value)}
className="text-sm" className="text-sm"
disabled={!pair.enabled} disabled={!pair.enabled}
/> />
</div> </div>
<div className="flex-[2]"> <div className="flex-[2]">
<Input <Input
placeholder="Value" placeholder="Value"
value={pair.value} value={pair.value}
onChange={(e) => updatePair(index, "value", e.target.value)} onChange={(e) => updatePair(index, "value", e.target.value)}
className="text-sm" className="text-sm"
disabled={!pair.enabled} disabled={!pair.enabled}
/> />
</div> </div>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => removePair(index)} onClick={() => removePair(index)}
className="text-red-500 hover:text-red-700 hover:bg-red-50" className="text-red-500 hover:text-red-700 hover:bg-red-50"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
))} ))}
</div> </div>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={addPair} onClick={addPair}
className="w-full" className="w-full"
> >
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Add {label.toLowerCase()} Add {label.toLowerCase()}
</Button> </Button>
</div> </div>
); );
}; };
@@ -1,164 +1,164 @@
import { import {
Bell, Bell,
Loader2, Loader2,
Mail, Mail,
MessageCircleMore, MessageCircleMore,
PenBoxIcon, PenBoxIcon,
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
DiscordIcon, DiscordIcon,
SlackIcon, SlackIcon,
TelegramIcon, TelegramIcon,
} from "@/components/icons/notification-icons"; } from "@/components/icons/notification-icons";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { HandleNotifications } from "./handle-notifications"; import { HandleNotifications } from "./handle-notifications";
export const ShowNotifications = () => { export const ShowNotifications = () => {
const { data, isLoading, refetch } = api.notification.all.useQuery(); const { data, isLoading, refetch } = api.notification.all.useQuery();
const { mutateAsync, isLoading: isRemoving } = const { mutateAsync, isLoading: isRemoving } =
api.notification.remove.useMutation(); api.notification.remove.useMutation();
return ( return (
<div className="w-full"> <div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto"> <Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md "> <div className="rounded-xl bg-background shadow-md ">
<CardHeader className=""> <CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2"> <CardTitle className="text-xl flex flex-row gap-2">
<Bell className="size-6 text-muted-foreground self-center" /> <Bell className="size-6 text-muted-foreground self-center" />
Notifications Notifications
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Add your providers to receive notifications, like Discord, Slack, Add your providers to receive notifications, like Discord, Slack,
Telegram, Email. Telegram, Email.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-2 py-8 border-t"> <CardContent className="space-y-2 py-8 border-t">
{isLoading ? ( {isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]"> <div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
<span>Loading...</span> <span>Loading...</span>
<Loader2 className="animate-spin size-4" /> <Loader2 className="animate-spin size-4" />
</div> </div>
) : ( ) : (
<> <>
{data?.length === 0 ? ( {data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center"> <div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<Bell /> <Bell />
<span className="text-base text-muted-foreground text-center"> <span className="text-base text-muted-foreground text-center">
To send notifications it is required to set at least 1 To send notifications it is required to set at least 1
provider. provider.
</span> </span>
<HandleNotifications /> <HandleNotifications />
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-4 min-h-[25vh]"> <div className="flex flex-col gap-4 min-h-[25vh]">
<div className="flex flex-col gap-4 rounded-lg "> <div className="flex flex-col gap-4 rounded-lg ">
{data?.map((notification, _index) => ( {data?.map((notification, _index) => (
<div <div
key={notification.notificationId} key={notification.notificationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<span className="text-sm flex flex-row items-center gap-4"> <span className="text-sm flex flex-row items-center gap-4">
{notification.notificationType === "slack" && ( {notification.notificationType === "slack" && (
<div className="flex items-center justify-center rounded-lg"> <div className="flex items-center justify-center rounded-lg">
<SlackIcon className="size-6" /> <SlackIcon className="size-6" />
</div> </div>
)} )}
{notification.notificationType === "telegram" && ( {notification.notificationType === "telegram" && (
<div className="flex items-center justify-center rounded-lg "> <div className="flex items-center justify-center rounded-lg ">
<TelegramIcon className="size-7 " /> <TelegramIcon className="size-7 " />
</div> </div>
)} )}
{notification.notificationType === "discord" && ( {notification.notificationType === "discord" && (
<div className="flex items-center justify-center rounded-lg"> <div className="flex items-center justify-center rounded-lg">
<DiscordIcon className="size-7 " /> <DiscordIcon className="size-7 " />
</div> </div>
)} )}
{notification.notificationType === "email" && ( {notification.notificationType === "email" && (
<div className="flex items-center justify-center rounded-lg "> <div className="flex items-center justify-center rounded-lg ">
<Mail className="size-6 text-muted-foreground" /> <Mail className="size-6 text-muted-foreground" />
</div> </div>
)} )}
{notification.notificationType === "gotify" && ( {notification.notificationType === "gotify" && (
<div className="flex items-center justify-center rounded-lg "> <div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" /> <MessageCircleMore className="size-6 text-muted-foreground" />
</div> </div>
)} )}
{notification.notificationType === "ntfy" && ( {notification.notificationType === "ntfy" && (
<div className="flex items-center justify-center rounded-lg "> <div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" /> <MessageCircleMore className="size-6 text-muted-foreground" />
</div> </div>
)} )}
{notification.notificationType === "custom" && ( {notification.notificationType === "custom" && (
<div className="flex items-center justify-center rounded-lg "> <div className="flex items-center justify-center rounded-lg ">
<PenBoxIcon className="size-6 text-muted-foreground" /> <PenBoxIcon className="size-6 text-muted-foreground" />
</div> </div>
)} )}
{notification.name} {notification.name}
</span> </span>
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
<HandleNotifications <HandleNotifications
notificationId={notification.notificationId} notificationId={notification.notificationId}
/> />
<DialogAction <DialogAction
title="Delete Notification" title="Delete Notification"
description="Are you sure you want to delete this notification?" description="Are you sure you want to delete this notification?"
type="destructive" type="destructive"
onClick={async () => { onClick={async () => {
await mutateAsync({ await mutateAsync({
notificationId: notification.notificationId, notificationId: notification.notificationId,
}) })
.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",
); );
}); });
}} }}
> >
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="group hover:bg-red-500/10 " className="group hover:bg-red-500/10 "
isLoading={isRemoving} isLoading={isRemoving}
> >
<Trash2 className="size-4 text-primary group-hover:text-red-500" /> <Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button> </Button>
</DialogAction> </DialogAction>
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4"> <div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
<HandleNotifications /> <HandleNotifications />
</div> </div>
</div> </div>
)} )}
</> </>
)} )}
</CardContent> </CardContent>
</div> </div>
</Card> </Card>
</div> </div>
); );
}; };
File diff suppressed because it is too large Load Diff
+309 -309
View File
@@ -6,400 +6,400 @@ import { z } from "zod";
import { organization } from "./account"; import { organization } from "./account";
export const notificationType = pgEnum("notificationType", [ export const notificationType = pgEnum("notificationType", [
"slack", "slack",
"telegram", "telegram",
"discord", "discord",
"email", "email",
"gotify", "gotify",
"ntfy", "ntfy",
"custom", "custom",
]); ]);
export const notifications = pgTable("notification", { export const notifications = pgTable("notification", {
notificationId: text("notificationId") notificationId: text("notificationId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
name: text("name").notNull(), name: text("name").notNull(),
appDeploy: boolean("appDeploy").notNull().default(false), appDeploy: boolean("appDeploy").notNull().default(false),
appBuildError: boolean("appBuildError").notNull().default(false), appBuildError: boolean("appBuildError").notNull().default(false),
databaseBackup: boolean("databaseBackup").notNull().default(false), databaseBackup: boolean("databaseBackup").notNull().default(false),
dokployRestart: boolean("dokployRestart").notNull().default(false), dokployRestart: boolean("dokployRestart").notNull().default(false),
dockerCleanup: boolean("dockerCleanup").notNull().default(false), dockerCleanup: boolean("dockerCleanup").notNull().default(false),
serverThreshold: boolean("serverThreshold").notNull().default(false), serverThreshold: boolean("serverThreshold").notNull().default(false),
notificationType: notificationType("notificationType").notNull(), notificationType: notificationType("notificationType").notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
slackId: text("slackId").references(() => slack.slackId, { slackId: text("slackId").references(() => slack.slackId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
telegramId: text("telegramId").references(() => telegram.telegramId, { telegramId: text("telegramId").references(() => telegram.telegramId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
discordId: text("discordId").references(() => discord.discordId, { discordId: text("discordId").references(() => discord.discordId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
emailId: text("emailId").references(() => email.emailId, { emailId: text("emailId").references(() => email.emailId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
gotifyId: text("gotifyId").references(() => gotify.gotifyId, { gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, { ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
customId: text("customId").references(() => custom.customId, { customId: text("customId").references(() => custom.customId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
organizationId: text("organizationId") organizationId: text("organizationId")
.notNull() .notNull()
.references(() => organization.id, { onDelete: "cascade" }), .references(() => organization.id, { onDelete: "cascade" }),
}); });
export const slack = pgTable("slack", { export const slack = pgTable("slack", {
slackId: text("slackId") slackId: text("slackId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
webhookUrl: text("webhookUrl").notNull(), webhookUrl: text("webhookUrl").notNull(),
channel: text("channel"), channel: text("channel"),
}); });
export const telegram = pgTable("telegram", { export const telegram = pgTable("telegram", {
telegramId: text("telegramId") telegramId: text("telegramId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
botToken: text("botToken").notNull(), botToken: text("botToken").notNull(),
chatId: text("chatId").notNull(), chatId: text("chatId").notNull(),
messageThreadId: text("messageThreadId"), messageThreadId: text("messageThreadId"),
}); });
export const discord = pgTable("discord", { export const discord = pgTable("discord", {
discordId: text("discordId") discordId: text("discordId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
webhookUrl: text("webhookUrl").notNull(), webhookUrl: text("webhookUrl").notNull(),
decoration: boolean("decoration"), decoration: boolean("decoration"),
}); });
export const email = pgTable("email", { export const email = pgTable("email", {
emailId: text("emailId") emailId: text("emailId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
smtpServer: text("smtpServer").notNull(), smtpServer: text("smtpServer").notNull(),
smtpPort: integer("smtpPort").notNull(), smtpPort: integer("smtpPort").notNull(),
username: text("username").notNull(), username: text("username").notNull(),
password: text("password").notNull(), password: text("password").notNull(),
fromAddress: text("fromAddress").notNull(), fromAddress: text("fromAddress").notNull(),
toAddresses: text("toAddress").array().notNull(), toAddresses: text("toAddress").array().notNull(),
}); });
export const gotify = pgTable("gotify", { export const gotify = pgTable("gotify", {
gotifyId: text("gotifyId") gotifyId: text("gotifyId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
serverUrl: text("serverUrl").notNull(), serverUrl: text("serverUrl").notNull(),
appToken: text("appToken").notNull(), appToken: text("appToken").notNull(),
priority: integer("priority").notNull().default(5), priority: integer("priority").notNull().default(5),
decoration: boolean("decoration"), decoration: boolean("decoration"),
}); });
export const ntfy = pgTable("ntfy", { export const ntfy = pgTable("ntfy", {
ntfyId: text("ntfyId") ntfyId: text("ntfyId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
serverUrl: text("serverUrl").notNull(), serverUrl: text("serverUrl").notNull(),
topic: text("topic").notNull(), topic: text("topic").notNull(),
accessToken: text("accessToken").notNull(), accessToken: text("accessToken").notNull(),
priority: integer("priority").notNull().default(3), priority: integer("priority").notNull().default(3),
}); });
export const custom = pgTable("custom", { export const custom = pgTable("custom", {
customId: text("customId") customId: text("customId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
endpoint: text("endpoint").notNull(), endpoint: text("endpoint").notNull(),
headers: text("headers"), // JSON string headers: text("headers"), // JSON string
}); });
export const notificationsRelations = relations(notifications, ({ one }) => ({ export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, { slack: one(slack, {
fields: [notifications.slackId], fields: [notifications.slackId],
references: [slack.slackId], references: [slack.slackId],
}), }),
telegram: one(telegram, { telegram: one(telegram, {
fields: [notifications.telegramId], fields: [notifications.telegramId],
references: [telegram.telegramId], references: [telegram.telegramId],
}), }),
discord: one(discord, { discord: one(discord, {
fields: [notifications.discordId], fields: [notifications.discordId],
references: [discord.discordId], references: [discord.discordId],
}), }),
email: one(email, { email: one(email, {
fields: [notifications.emailId], fields: [notifications.emailId],
references: [email.emailId], references: [email.emailId],
}), }),
gotify: one(gotify, { gotify: one(gotify, {
fields: [notifications.gotifyId], fields: [notifications.gotifyId],
references: [gotify.gotifyId], references: [gotify.gotifyId],
}), }),
ntfy: one(ntfy, { ntfy: one(ntfy, {
fields: [notifications.ntfyId], fields: [notifications.ntfyId],
references: [ntfy.ntfyId], references: [ntfy.ntfyId],
}), }),
custom: one(custom, { custom: one(custom, {
fields: [notifications.customId], fields: [notifications.customId],
references: [custom.customId], references: [custom.customId],
}), }),
organization: one(organization, { organization: one(organization, {
fields: [notifications.organizationId], fields: [notifications.organizationId],
references: [organization.id], references: [organization.id],
}), }),
})); }));
export const notificationsSchema = createInsertSchema(notifications); export const notificationsSchema = createInsertSchema(notifications);
export const apiCreateSlack = notificationsSchema export const apiCreateSlack = notificationsSchema
.pick({ .pick({
appBuildError: true, appBuildError: true,
databaseBackup: true, databaseBackup: true,
dokployRestart: true, dokployRestart: true,
name: true, name: true,
appDeploy: true, appDeploy: true,
dockerCleanup: true, dockerCleanup: true,
serverThreshold: true, serverThreshold: true,
}) })
.extend({ .extend({
webhookUrl: z.string().min(1), webhookUrl: z.string().min(1),
channel: z.string(), channel: z.string(),
}) })
.required(); .required();
export const apiUpdateSlack = apiCreateSlack.partial().extend({ export const apiUpdateSlack = apiCreateSlack.partial().extend({
notificationId: z.string().min(1), notificationId: z.string().min(1),
slackId: z.string(), slackId: z.string(),
organizationId: z.string().optional(), organizationId: z.string().optional(),
}); });
export const apiTestSlackConnection = apiCreateSlack.pick({ export const apiTestSlackConnection = apiCreateSlack.pick({
webhookUrl: true, webhookUrl: true,
channel: true, channel: true,
}); });
export const apiCreateTelegram = notificationsSchema export const apiCreateTelegram = notificationsSchema
.pick({ .pick({
appBuildError: true, appBuildError: true,
databaseBackup: true, databaseBackup: true,
dokployRestart: true, dokployRestart: true,
name: true, name: true,
appDeploy: true, appDeploy: true,
dockerCleanup: true, dockerCleanup: true,
serverThreshold: true, serverThreshold: true,
}) })
.extend({ .extend({
botToken: z.string().min(1), botToken: z.string().min(1),
chatId: z.string().min(1), chatId: z.string().min(1),
messageThreadId: z.string(), messageThreadId: z.string(),
}) })
.required(); .required();
export const apiUpdateTelegram = apiCreateTelegram.partial().extend({ export const apiUpdateTelegram = apiCreateTelegram.partial().extend({
notificationId: z.string().min(1), notificationId: z.string().min(1),
telegramId: z.string().min(1), telegramId: z.string().min(1),
organizationId: z.string().optional(), organizationId: z.string().optional(),
}); });
export const apiTestTelegramConnection = apiCreateTelegram.pick({ export const apiTestTelegramConnection = apiCreateTelegram.pick({
botToken: true, botToken: true,
chatId: true, chatId: true,
messageThreadId: true, messageThreadId: true,
}); });
export const apiCreateDiscord = notificationsSchema export const apiCreateDiscord = notificationsSchema
.pick({ .pick({
appBuildError: true, appBuildError: true,
databaseBackup: true, databaseBackup: true,
dokployRestart: true, dokployRestart: true,
name: true, name: true,
appDeploy: true, appDeploy: true,
dockerCleanup: true, dockerCleanup: true,
serverThreshold: true, serverThreshold: true,
}) })
.extend({ .extend({
webhookUrl: z.string().min(1), webhookUrl: z.string().min(1),
decoration: z.boolean(), decoration: z.boolean(),
}) })
.required(); .required();
export const apiUpdateDiscord = apiCreateDiscord.partial().extend({ export const apiUpdateDiscord = apiCreateDiscord.partial().extend({
notificationId: z.string().min(1), notificationId: z.string().min(1),
discordId: z.string().min(1), discordId: z.string().min(1),
organizationId: z.string().optional(), organizationId: z.string().optional(),
}); });
export const apiTestDiscordConnection = apiCreateDiscord export const apiTestDiscordConnection = apiCreateDiscord
.pick({ .pick({
webhookUrl: true, webhookUrl: true,
}) })
.extend({ .extend({
decoration: z.boolean().optional(), decoration: z.boolean().optional(),
}); });
export const apiCreateEmail = notificationsSchema export const apiCreateEmail = notificationsSchema
.pick({ .pick({
appBuildError: true, appBuildError: true,
databaseBackup: true, databaseBackup: true,
dokployRestart: true, dokployRestart: true,
name: true, name: true,
appDeploy: true, appDeploy: true,
dockerCleanup: true, dockerCleanup: true,
serverThreshold: true, serverThreshold: true,
}) })
.extend({ .extend({
smtpServer: z.string().min(1), smtpServer: z.string().min(1),
smtpPort: z.number().min(1), smtpPort: z.number().min(1),
username: z.string().min(1), username: z.string().min(1),
password: z.string().min(1), password: z.string().min(1),
fromAddress: z.string().min(1), fromAddress: z.string().min(1),
toAddresses: z.array(z.string()).min(1), toAddresses: z.array(z.string()).min(1),
}) })
.required(); .required();
export const apiUpdateEmail = apiCreateEmail.partial().extend({ export const apiUpdateEmail = apiCreateEmail.partial().extend({
notificationId: z.string().min(1), notificationId: z.string().min(1),
emailId: z.string().min(1), emailId: z.string().min(1),
organizationId: z.string().optional(), organizationId: z.string().optional(),
}); });
export const apiTestEmailConnection = apiCreateEmail.pick({ export const apiTestEmailConnection = apiCreateEmail.pick({
smtpServer: true, smtpServer: true,
smtpPort: true, smtpPort: true,
username: true, username: true,
password: true, password: true,
toAddresses: true, toAddresses: true,
fromAddress: true, fromAddress: true,
}); });
export const apiCreateGotify = notificationsSchema export const apiCreateGotify = notificationsSchema
.pick({ .pick({
appBuildError: true, appBuildError: true,
databaseBackup: true, databaseBackup: true,
dokployRestart: true, dokployRestart: true,
name: true, name: true,
appDeploy: true, appDeploy: true,
dockerCleanup: true, dockerCleanup: true,
}) })
.extend({ .extend({
serverUrl: z.string().min(1), serverUrl: z.string().min(1),
appToken: z.string().min(1), appToken: z.string().min(1),
priority: z.number().min(1), priority: z.number().min(1),
decoration: z.boolean(), decoration: z.boolean(),
}) })
.required(); .required();
export const apiUpdateGotify = apiCreateGotify.partial().extend({ export const apiUpdateGotify = apiCreateGotify.partial().extend({
notificationId: z.string().min(1), notificationId: z.string().min(1),
gotifyId: z.string().min(1), gotifyId: z.string().min(1),
organizationId: z.string().optional(), organizationId: z.string().optional(),
}); });
export const apiTestGotifyConnection = apiCreateGotify export const apiTestGotifyConnection = apiCreateGotify
.pick({ .pick({
serverUrl: true, serverUrl: true,
appToken: true, appToken: true,
priority: true, priority: true,
}) })
.extend({ .extend({
decoration: z.boolean().optional(), decoration: z.boolean().optional(),
}); });
export const apiCreateNtfy = notificationsSchema export const apiCreateNtfy = notificationsSchema
.pick({ .pick({
appBuildError: true, appBuildError: true,
databaseBackup: true, databaseBackup: true,
dokployRestart: true, dokployRestart: true,
name: true, name: true,
appDeploy: true, appDeploy: true,
dockerCleanup: true, dockerCleanup: true,
}) })
.extend({ .extend({
serverUrl: z.string().min(1), serverUrl: z.string().min(1),
topic: z.string().min(1), topic: z.string().min(1),
accessToken: z.string().min(1), accessToken: z.string().min(1),
priority: z.number().min(1), priority: z.number().min(1),
}) })
.required(); .required();
export const apiUpdateNtfy = apiCreateNtfy.partial().extend({ export const apiUpdateNtfy = apiCreateNtfy.partial().extend({
notificationId: z.string().min(1), notificationId: z.string().min(1),
ntfyId: z.string().min(1), ntfyId: z.string().min(1),
organizationId: z.string().optional(), organizationId: z.string().optional(),
}); });
export const apiTestNtfyConnection = apiCreateNtfy.pick({ export const apiTestNtfyConnection = apiCreateNtfy.pick({
serverUrl: true, serverUrl: true,
topic: true, topic: true,
accessToken: true, accessToken: true,
priority: true, priority: true,
}); });
export const apiFindOneNotification = notificationsSchema export const apiFindOneNotification = notificationsSchema
.pick({ .pick({
notificationId: true, notificationId: true,
}) })
.required(); .required();
export const apiCreateCustom = notificationsSchema export const apiCreateCustom = notificationsSchema
.pick({ .pick({
appBuildError: true, appBuildError: true,
databaseBackup: true, databaseBackup: true,
dokployRestart: true, dokployRestart: true,
name: true, name: true,
appDeploy: true, appDeploy: true,
dockerCleanup: true, dockerCleanup: true,
serverThreshold: true, serverThreshold: true,
}) })
.extend({ .extend({
endpoint: z.string().min(1), endpoint: z.string().min(1),
headers: z.string().optional(), headers: z.string().optional(),
}); });
export const apiUpdateCustom = apiCreateCustom.partial().extend({ export const apiUpdateCustom = apiCreateCustom.partial().extend({
notificationId: z.string().min(1), notificationId: z.string().min(1),
customId: z.string().min(1), customId: z.string().min(1),
organizationId: z.string().optional(), organizationId: z.string().optional(),
}); });
export const apiTestCustomConnection = z.object({ export const apiTestCustomConnection = z.object({
endpoint: z.string().min(1), endpoint: z.string().min(1),
headers: z.string().optional(), headers: z.string().optional(),
}); });
export const apiSendTest = notificationsSchema export const apiSendTest = notificationsSchema
.extend({ .extend({
botToken: z.string(), botToken: z.string(),
chatId: z.string(), chatId: z.string(),
webhookUrl: z.string(), webhookUrl: z.string(),
channel: z.string(), channel: z.string(),
smtpServer: z.string(), smtpServer: z.string(),
smtpPort: z.number(), smtpPort: z.number(),
fromAddress: z.string(), fromAddress: z.string(),
username: z.string(), username: z.string(),
password: z.string(), password: z.string(),
toAddresses: z.array(z.string()), toAddresses: z.array(z.string()),
serverUrl: z.string(), serverUrl: z.string(),
topic: z.string(), topic: z.string(),
appToken: z.string(), appToken: z.string(),
accessToken: z.string(), accessToken: z.string(),
priority: z.number(), priority: z.number(),
endpoint: z.string(), endpoint: z.string(),
headers: z.string(), headers: z.string(),
}) })
.partial(); .partial();
File diff suppressed because it is too large Load Diff
@@ -5,236 +5,236 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns"; import { format } from "date-fns";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { import {
sendCustomNotification, sendCustomNotification,
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification, sendGotifyNotification,
sendNtfyNotification, sendNtfyNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
interface Props { interface Props {
projectName: string; projectName: string;
applicationName: string; applicationName: string;
applicationType: string; applicationType: string;
errorMessage: string; errorMessage: string;
buildLink: string; buildLink: string;
organizationId: string; organizationId: string;
} }
export const sendBuildErrorNotifications = async ({ export const sendBuildErrorNotifications = async ({
projectName, projectName,
applicationName, applicationName,
applicationType, applicationType,
errorMessage, errorMessage,
buildLink, buildLink,
organizationId, organizationId,
}: Props) => { }: Props) => {
const date = new Date(); const date = new Date();
const unixDate = ~~(Number(date) / 1000); const unixDate = ~~(Number(date) / 1000);
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,
discord: true, discord: true,
telegram: true, telegram: true,
slack: true, slack: true,
gotify: true, gotify: true,
ntfy: true, ntfy: true,
custom: true, custom: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } = const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification; notification;
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
BuildFailedEmail({ BuildFailedEmail({
projectName, projectName,
applicationName, applicationName,
applicationType, applicationType,
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);
} }
if (discord) { if (discord) {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim(); `${discord.decoration ? decoration : ""} ${text}`.trim();
const limitCharacter = 800; const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendDiscordNotification(discord, { await sendDiscordNotification(discord, {
title: decorate(">", "`⚠️` Build Failed"), title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245, color: 0xed4245,
fields: [ fields: [
{ {
name: decorate("`🛠️`", "Project"), name: decorate("`🛠️`", "Project"),
value: projectName, value: projectName,
inline: true, inline: true,
}, },
{ {
name: decorate("`⚙️`", "Application"), name: decorate("`⚙️`", "Application"),
value: applicationName, value: applicationName,
inline: true, inline: true,
}, },
{ {
name: decorate("`❔`", "Type"), name: decorate("`❔`", "Type"),
value: applicationType, value: applicationType,
inline: true, inline: true,
}, },
{ {
name: decorate("`📅`", "Date"), name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`, value: `<t:${unixDate}:D>`,
inline: true, inline: true,
}, },
{ {
name: decorate("`⌚`", "Time"), name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`, value: `<t:${unixDate}:t>`,
inline: true, inline: true,
}, },
{ {
name: decorate("`❓`", "Type"), name: decorate("`❓`", "Type"),
value: "Failed", value: "Failed",
inline: true, inline: true,
}, },
{ {
name: decorate("`⚠️`", "Error Message"), name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${truncatedErrorMessage}\`\`\``, value: `\`\`\`${truncatedErrorMessage}\`\`\``,
}, },
{ {
name: decorate("`🧷`", "Build Link"), name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`, value: `[Click here to access build link](${buildLink})`,
}, },
], ],
timestamp: date.toISOString(), timestamp: date.toISOString(),
footer: { footer: {
text: "Dokploy Build Notification", text: "Dokploy Build Notification",
}, },
}); });
} }
if (gotify) { if (gotify) {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`; `${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification( await sendGotifyNotification(
gotify, gotify,
decorate("⚠️", "Build Failed"), decorate("⚠️", "Build Failed"),
`${decorate("🛠️", `Project: ${projectName}`)}` + `${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` + `${decorate("⚙️", `Application: ${applicationName}`)}` +
`${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}`)}`,
); );
} }
if (ntfy) { if (ntfy) {
await sendNtfyNotification( await sendNtfyNotification(
ntfy, ntfy,
"Build Failed", "Build Failed",
"warning", "warning",
`view, Build details, ${buildLink}, clear=true;`, `view, Build details, ${buildLink}, clear=true;`,
`🛠️Project: ${projectName}\n` + `🛠️Project: ${projectName}\n` +
`⚙️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}`,
); );
} }
if (telegram) { if (telegram) {
const inlineButton = [ const inlineButton = [
[ [
{ {
text: "Deployment Logs", text: "Deployment Logs",
url: buildLink, url: buildLink,
}, },
], ],
]; ];
await sendTelegramNotification( await sendTelegramNotification(
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,
); );
} }
if (slack) { if (slack) {
const { channel } = slack; const { channel } = slack;
await sendSlackNotification(slack, { await sendSlackNotification(slack, {
channel: channel, channel: channel,
attachments: [ attachments: [
{ {
color: "#FF0000", color: "#FF0000",
pretext: ":warning: *Build Failed*", pretext: ":warning: *Build Failed*",
fields: [ fields: [
{ {
title: "Project", title: "Project",
value: projectName, value: projectName,
short: true, short: true,
}, },
{ {
title: "Application", title: "Application",
value: applicationName, value: applicationName,
short: true, short: true,
}, },
{ {
title: "Type", title: "Type",
value: applicationType, value: applicationType,
short: true, short: true,
}, },
{ {
title: "Time", title: "Time",
value: date.toLocaleString(), value: date.toLocaleString(),
short: true, short: true,
}, },
{ {
title: "Error", title: "Error",
value: `\`\`\`${errorMessage}\`\`\``, value: `\`\`\`${errorMessage}\`\`\``,
short: false, short: false,
}, },
], ],
actions: [ actions: [
{ {
type: "button", type: "button",
text: "View Build Details", text: "View Build Details",
url: buildLink, url: buildLink,
}, },
], ],
}, },
], ],
}); });
} }
if (custom) { if (custom) {
await sendCustomNotification(custom, { await sendCustomNotification(custom, {
title: "Build Error", title: "Build Error",
message: "Build failed with errors", message: "Build failed with errors",
projectName, projectName,
applicationName, applicationName,
applicationType, applicationType,
errorMessage, errorMessage,
buildLink, buildLink,
timestamp: date.toISOString(), timestamp: date.toISOString(),
date: date.toLocaleString(), date: date.toLocaleString(),
status: "error", status: "error",
type: "build", type: "build",
}); });
} }
} }
}; };
@@ -6,231 +6,231 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns"; import { format } from "date-fns";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { import {
sendCustomNotification, sendCustomNotification,
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification, sendGotifyNotification,
sendNtfyNotification, sendNtfyNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
interface Props { interface Props {
projectName: string; projectName: string;
applicationName: string; applicationName: string;
applicationType: string; applicationType: string;
buildLink: string; buildLink: string;
organizationId: string; organizationId: string;
domains: Domain[]; domains: Domain[];
} }
export const sendBuildSuccessNotifications = async ({ export const sendBuildSuccessNotifications = async ({
projectName, projectName,
applicationName, applicationName,
applicationType, applicationType,
buildLink, buildLink,
organizationId, organizationId,
domains, domains,
}: Props) => { }: Props) => {
const date = new Date(); const date = new Date();
const unixDate = ~~(Number(date) / 1000); const unixDate = ~~(Number(date) / 1000);
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,
discord: true, discord: true,
telegram: true, telegram: true,
slack: true, slack: true,
gotify: true, gotify: true,
ntfy: true, ntfy: true,
custom: true, custom: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } = const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification; notification;
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
BuildSuccessEmail({ BuildSuccessEmail({
projectName, projectName,
applicationName, applicationName,
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);
} }
if (discord) { if (discord) {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim(); `${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, { await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Build Success"), title: decorate(">", "`✅` Build Success"),
color: 0x57f287, color: 0x57f287,
fields: [ fields: [
{ {
name: decorate("`🛠️`", "Project"), name: decorate("`🛠️`", "Project"),
value: projectName, value: projectName,
inline: true, inline: true,
}, },
{ {
name: decorate("`⚙️`", "Application"), name: decorate("`⚙️`", "Application"),
value: applicationName, value: applicationName,
inline: true, inline: true,
}, },
{ {
name: decorate("`❔`", "Type"), name: decorate("`❔`", "Type"),
value: applicationType, value: applicationType,
inline: true, inline: true,
}, },
{ {
name: decorate("`📅`", "Date"), name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`, value: `<t:${unixDate}:D>`,
inline: true, inline: true,
}, },
{ {
name: decorate("`⌚`", "Time"), name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`, value: `<t:${unixDate}:t>`,
inline: true, inline: true,
}, },
{ {
name: decorate("`❓`", "Type"), name: decorate("`❓`", "Type"),
value: "Successful", value: "Successful",
inline: true, inline: true,
}, },
{ {
name: decorate("`🧷`", "Build Link"), name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`, value: `[Click here to access build link](${buildLink})`,
}, },
], ],
timestamp: date.toISOString(), timestamp: date.toISOString(),
footer: { footer: {
text: "Dokploy Build Notification", text: "Dokploy Build Notification",
}, },
}); });
} }
if (gotify) { if (gotify) {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`; `${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification( await sendGotifyNotification(
gotify, gotify,
decorate("✅", "Build Success"), decorate("✅", "Build Success"),
`${decorate("🛠️", `Project: ${projectName}`)}` + `${decorate("🛠️", `Project: ${projectName}`)}` +
`${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}`)}`,
); );
} }
if (ntfy) { if (ntfy) {
await sendNtfyNotification( await sendNtfyNotification(
ntfy, ntfy,
"Build Success", "Build Success",
"white_check_mark", "white_check_mark",
`view, Build details, ${buildLink}, clear=true;`, `view, Build details, ${buildLink}, clear=true;`,
`🛠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 = [
[ [
{ {
text: "Deployment Logs", text: "Deployment Logs",
url: buildLink, url: buildLink,
}, },
], ],
...chunkArray(domains, 2).map((chunk) => ...chunkArray(domains, 2).map((chunk) =>
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}`,
})) })),
), ),
]; ];
await sendTelegramNotification( await sendTelegramNotification(
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,
); );
} }
if (slack) { if (slack) {
const { channel } = slack; const { channel } = slack;
await sendSlackNotification(slack, { await sendSlackNotification(slack, {
channel: channel, channel: channel,
attachments: [ attachments: [
{ {
color: "#00FF00", color: "#00FF00",
pretext: ":white_check_mark: *Build Success*", pretext: ":white_check_mark: *Build Success*",
fields: [ fields: [
{ {
title: "Project", title: "Project",
value: projectName, value: projectName,
short: true, short: true,
}, },
{ {
title: "Application", title: "Application",
value: applicationName, value: applicationName,
short: true, short: true,
}, },
{ {
title: "Type", title: "Type",
value: applicationType, value: applicationType,
short: true, short: true,
}, },
{ {
title: "Time", title: "Time",
value: date.toLocaleString(), value: date.toLocaleString(),
short: true, short: true,
}, },
], ],
actions: [ actions: [
{ {
type: "button", type: "button",
text: "View Build Details", text: "View Build Details",
url: buildLink, url: buildLink,
}, },
], ],
}, },
], ],
}); });
} }
if (custom) { if (custom) {
await sendCustomNotification(custom, { await sendCustomNotification(custom, {
title: "Build Success", title: "Build Success",
message: "Build completed successfully", message: "Build completed successfully",
projectName, projectName,
applicationName, applicationName,
applicationType, applicationType,
buildLink, buildLink,
timestamp: date.toISOString(), timestamp: date.toISOString(),
date: date.toLocaleString(), date: date.toLocaleString(),
domains: domains.map((domain) => domain.host).join(", "), domains: domains.map((domain) => domain.host).join(", "),
status: "success", status: "success",
type: "build", type: "build",
}); });
} }
} }
}; };
@@ -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(
+155 -155
View File
@@ -1,193 +1,193 @@
import type { import type {
custom, custom,
discord, discord,
email, email,
gotify, gotify,
ntfy, ntfy,
slack, slack,
telegram, telegram,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import nodemailer from "nodemailer"; 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 {
smtpServer, smtpServer,
smtpPort, smtpPort,
username, username,
password, password,
fromAddress, fromAddress,
toAddresses, toAddresses,
} = connection; } = connection;
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: smtpServer, host: smtpServer,
port: smtpPort, port: smtpPort,
auth: { user: username, pass: password }, 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) { } catch (err) {
console.log(err); console.log(err);
} }
}; };
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, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [embed] }), body: JSON.stringify({ embeds: [embed] }),
}); });
// } catch (err) { // } catch (err) {
// console.log(err); // console.log(err);
// } // }
}; };
export const sendTelegramNotification = async ( export const sendTelegramNotification = async (
connection: typeof telegram.$inferInsert, connection: typeof telegram.$inferInsert,
messageText: string, messageText: string,
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`;
await fetch(url, { await fetch(url, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
chat_id: connection.chatId, chat_id: connection.chatId,
message_thread_id: connection.messageThreadId, message_thread_id: connection.messageThreadId,
text: messageText, text: messageText,
parse_mode: "HTML", parse_mode: "HTML",
disable_web_page_preview: true, disable_web_page_preview: true,
reply_markup: { reply_markup: {
inline_keyboard: inlineButton, inline_keyboard: inlineButton,
}, },
}), }),
}); });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
}; };
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, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(message), body: JSON.stringify(message),
}); });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
}; };
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",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Gotify-Key": connection.appToken, "X-Gotify-Key": connection.appToken,
}, },
body: JSON.stringify({ body: JSON.stringify({
title: title, title: title,
message: message, message: message,
priority: connection.priority, priority: connection.priority,
extras: { extras: {
"client::display": { "client::display": {
contentType: "text/plain", contentType: "text/plain",
}, },
}, },
}), }),
}); });
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}`,
); );
} }
}; };
export const sendNtfyNotification = async ( export const sendNtfyNotification = async (
connection: typeof ntfy.$inferInsert, connection: typeof ntfy.$inferInsert,
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",
headers: { headers: {
Authorization: `Bearer ${connection.accessToken}`, Authorization: `Bearer ${connection.accessToken}`,
"X-Priority": connection.priority?.toString() || "3", "X-Priority": connection.priority?.toString() || "3",
"X-Title": title, "X-Title": title,
"X-Tags": tags, "X-Tags": tags,
"X-Actions": actions, "X-Actions": actions,
}, },
body: message, body: message,
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to send ntfy notification: ${response.statusText}`); throw new Error(`Failed to send ntfy notification: ${response.statusText}`);
} }
}; };
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
let headers: Record<string, string> = { let headers: Record<string, string> = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
if (connection.headers) { if (connection.headers) {
try { try {
headers = { ...headers, ...JSON.parse(connection.headers) }; headers = { ...headers, ...JSON.parse(connection.headers) };
} catch (error) { } catch (error) {
console.error("Error parsing headers:", error); console.error("Error parsing headers:", error);
} }
} }
// Default body with payload // Default body with payload
const body = JSON.stringify(payload); const body = JSON.stringify(payload);
const response = await fetch(connection.endpoint, { const response = await fetch(connection.endpoint, {
method: "POST", method: "POST",
headers, headers,
body, body,
}); });
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}`,
); );
} }
return response; return response;
} catch (error) { } catch (error) {
console.error("Error sending custom notification:", error); console.error("Error sending custom notification:", error);
throw error; throw error;
} }
}; };