feat(notifications): add emails and methos for each action

This commit is contained in:
Mauricio Siu
2024-07-19 01:02:48 -06:00
parent 787506fb6b
commit 2d4eaeb8b5
22 changed files with 7215 additions and 162 deletions

View File

@@ -1,3 +1,8 @@
import {
DiscordIcon,
SlackIcon,
TelegramIcon,
} from "@/components/icons/notification-icons";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -18,31 +23,26 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Mail } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form"; import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { AlertTriangle, Mail } from "lucide-react";
import {
DiscordIcon,
SlackIcon,
TelegramIcon,
} from "@/components/icons/notification-icons";
import { Switch } from "@/components/ui/switch";
const notificationBaseSchema = z.object({ const notificationBaseSchema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
message: "Name is required", message: "Name is required",
}), }),
appDeploy: z.boolean().default(false), appDeploy: z.boolean().default(false),
userJoin: z.boolean().default(false), appBuildError: z.boolean().default(false),
appBuilderError: z.boolean().default(false),
databaseBackup: z.boolean().default(false), databaseBackup: z.boolean().default(false),
dokployRestart: z.boolean().default(false), dokployRestart: z.boolean().default(false),
dockerCleanup: z.boolean().default(false),
}); });
export const notificationSchema = z.discriminatedUnion("type", [ export const notificationSchema = z.discriminatedUnion("type", [
@@ -159,52 +159,51 @@ export const AddNotification = () => {
const onSubmit = async (data: NotificationSchema) => { const onSubmit = async (data: NotificationSchema) => {
const { const {
appBuilderError, appBuildError,
appDeploy, appDeploy,
dokployRestart, dokployRestart,
databaseBackup, databaseBackup,
userJoin, dockerCleanup,
} = data; } = data;
let promise: Promise<unknown> | null = null; let promise: Promise<unknown> | null = null;
if (data.type === "slack") { if (data.type === "slack") {
promise = slackMutation.mutateAsync({ promise = slackMutation.mutateAsync({
appBuildError: appBuilderError, appBuildError: appBuildError,
appDeploy: appDeploy, appDeploy: appDeploy,
dokployRestart: dokployRestart, dokployRestart: dokployRestart,
databaseBackup: databaseBackup, databaseBackup: databaseBackup,
userJoin: userJoin,
webhookUrl: data.webhookUrl, webhookUrl: data.webhookUrl,
channel: data.channel, channel: data.channel,
name: data.name, name: data.name,
dockerCleanup: dockerCleanup,
}); });
} else if (data.type === "telegram") { } else if (data.type === "telegram") {
promise = telegramMutation.mutateAsync({ promise = telegramMutation.mutateAsync({
appBuildError: appBuilderError, appBuildError: appBuildError,
appDeploy: appDeploy, appDeploy: appDeploy,
dokployRestart: dokployRestart, dokployRestart: dokployRestart,
databaseBackup: databaseBackup, databaseBackup: databaseBackup,
userJoin: userJoin,
botToken: data.botToken, botToken: data.botToken,
chatId: data.chatId, chatId: data.chatId,
name: data.name, name: data.name,
dockerCleanup: dockerCleanup,
}); });
} else if (data.type === "discord") { } else if (data.type === "discord") {
promise = discordMutation.mutateAsync({ promise = discordMutation.mutateAsync({
appBuildError: appBuilderError, appBuildError: appBuildError,
appDeploy: appDeploy, appDeploy: appDeploy,
dokployRestart: dokployRestart, dokployRestart: dokployRestart,
databaseBackup: databaseBackup, databaseBackup: databaseBackup,
userJoin: userJoin,
webhookUrl: data.webhookUrl, webhookUrl: data.webhookUrl,
name: data.name, name: data.name,
dockerCleanup: dockerCleanup,
}); });
} else if (data.type === "email") { } else if (data.type === "email") {
promise = emailMutation.mutateAsync({ promise = emailMutation.mutateAsync({
appBuildError: appBuilderError, appBuildError: appBuildError,
appDeploy: appDeploy, appDeploy: appDeploy,
dokployRestart: dokployRestart, dokployRestart: dokployRestart,
databaseBackup: databaseBackup, databaseBackup: databaseBackup,
userJoin: userJoin,
smtpServer: data.smtpServer, smtpServer: data.smtpServer,
smtpPort: data.smtpPort, smtpPort: data.smtpPort,
username: data.username, username: data.username,
@@ -212,6 +211,7 @@ export const AddNotification = () => {
fromAddress: data.fromAddress, fromAddress: data.fromAddress,
toAddresses: data.toAddresses, toAddresses: data.toAddresses,
name: data.name, name: data.name,
dockerCleanup: dockerCleanup,
}); });
} }
@@ -598,13 +598,13 @@ export const AddNotification = () => {
/> />
<FormField <FormField
control={form.control} control={form.control}
name="userJoin" name="appBuildError"
render={({ field }) => ( render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2"> <FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>User Join</FormLabel> <FormLabel>App Build Error</FormLabel>
<FormDescription> <FormDescription>
Trigger the action when a user joins the app. Trigger the action when the build fails.
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>
@@ -616,6 +616,7 @@ export const AddNotification = () => {
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="databaseBackup" name="databaseBackup"
@@ -639,13 +640,14 @@ export const AddNotification = () => {
<FormField <FormField
control={form.control} control={form.control}
name="dokployRestart" name="dockerCleanup"
render={({ field }) => ( render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2"> <FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Deploy Restart</FormLabel> <FormLabel>Docker Cleanup</FormLabel>
<FormDescription> <FormDescription>
Trigger the action when a deploy is restarted. Trigger the action when the docker cleanup is
performed.
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>
@@ -657,15 +659,16 @@ export const AddNotification = () => {
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="appBuilderError" name="dokployRestart"
render={({ field }) => ( render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2"> <FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>App Builder Error</FormLabel> <FormLabel>Dokploy Restart</FormLabel>
<FormDescription> <FormDescription>
Trigger the action when the build fails. Trigger the action when a dokploy is restarted.
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>

View File

@@ -1,3 +1,8 @@
import {
DiscordIcon,
SlackIcon,
TelegramIcon,
} from "@/components/icons/notification-icons";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -18,21 +23,16 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Mail, PenBoxIcon } from "lucide-react"; import { Mail, PenBoxIcon } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { FieldErrors, useFieldArray, useForm } from "react-hook-form"; import { FieldErrors, useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { Switch } from "@/components/ui/switch";
import { import {
TelegramIcon,
DiscordIcon,
SlackIcon,
} from "@/components/icons/notification-icons";
import {
notificationSchema,
type NotificationSchema, type NotificationSchema,
notificationSchema,
} from "./add-notification"; } from "./add-notification";
interface Props { interface Props {
@@ -82,11 +82,11 @@ export const UpdateNotification = ({ notificationId }: Props) => {
if (data) { if (data) {
if (data.notificationType === "slack") { if (data.notificationType === "slack") {
form.reset({ form.reset({
appBuilderError: data.appBuildError, appBuildError: data.appBuildError,
appDeploy: data.appDeploy, appDeploy: data.appDeploy,
dokployRestart: data.dokployRestart, dokployRestart: data.dokployRestart,
databaseBackup: data.databaseBackup, databaseBackup: data.databaseBackup,
userJoin: data.userJoin, dockerCleanup: data.dockerCleanup,
webhookUrl: data.slack?.webhookUrl, webhookUrl: data.slack?.webhookUrl,
channel: data.slack?.channel || "", channel: data.slack?.channel || "",
name: data.name, name: data.name,
@@ -94,35 +94,34 @@ export const UpdateNotification = ({ notificationId }: Props) => {
}); });
} else if (data.notificationType === "telegram") { } else if (data.notificationType === "telegram") {
form.reset({ form.reset({
appBuilderError: data.appBuildError, appBuildError: data.appBuildError,
appDeploy: data.appDeploy, appDeploy: data.appDeploy,
dokployRestart: data.dokployRestart, dokployRestart: data.dokployRestart,
databaseBackup: data.databaseBackup, databaseBackup: data.databaseBackup,
userJoin: data.userJoin,
botToken: data.telegram?.botToken, botToken: data.telegram?.botToken,
chatId: data.telegram?.chatId, chatId: data.telegram?.chatId,
type: data.notificationType, type: data.notificationType,
name: data.name, name: data.name,
dockerCleanup: data.dockerCleanup,
}); });
} else if (data.notificationType === "discord") { } else if (data.notificationType === "discord") {
form.reset({ form.reset({
appBuilderError: data.appBuildError, appBuildError: data.appBuildError,
appDeploy: data.appDeploy, appDeploy: data.appDeploy,
dokployRestart: data.dokployRestart, dokployRestart: data.dokployRestart,
databaseBackup: data.databaseBackup, databaseBackup: data.databaseBackup,
userJoin: data.userJoin,
type: data.notificationType, type: data.notificationType,
webhookUrl: data.discord?.webhookUrl, webhookUrl: data.discord?.webhookUrl,
name: data.name, name: data.name,
dockerCleanup: data.dockerCleanup,
}); });
} else if (data.notificationType === "email") { } else if (data.notificationType === "email") {
form.reset({ form.reset({
appBuilderError: data.appBuildError, appBuildError: data.appBuildError,
appDeploy: data.appDeploy, appDeploy: data.appDeploy,
dokployRestart: data.dokployRestart, dokployRestart: data.dokployRestart,
databaseBackup: data.databaseBackup, databaseBackup: data.databaseBackup,
type: data.notificationType, type: data.notificationType,
userJoin: data.userJoin,
smtpServer: data.email?.smtpServer, smtpServer: data.email?.smtpServer,
smtpPort: data.email?.smtpPort, smtpPort: data.email?.smtpPort,
username: data.email?.username, username: data.email?.username,
@@ -130,6 +129,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
toAddresses: data.email?.toAddresses, toAddresses: data.email?.toAddresses,
fromAddress: data.email?.fromAddress, fromAddress: data.email?.fromAddress,
name: data.name, name: data.name,
dockerCleanup: data.dockerCleanup,
}); });
} }
} }
@@ -137,58 +137,57 @@ export const UpdateNotification = ({ notificationId }: Props) => {
const onSubmit = async (formData: NotificationSchema) => { const onSubmit = async (formData: NotificationSchema) => {
const { const {
appBuilderError, appBuildError,
appDeploy, appDeploy,
dokployRestart, dokployRestart,
databaseBackup, databaseBackup,
userJoin, dockerCleanup,
} = formData; } = formData;
let promise: Promise<unknown> | null = null; let promise: Promise<unknown> | null = null;
if (formData?.type === "slack" && data?.slackId) { if (formData?.type === "slack" && data?.slackId) {
promise = slackMutation.mutateAsync({ promise = slackMutation.mutateAsync({
appBuildError: appBuilderError, appBuildError: appBuildError,
appDeploy: appDeploy, appDeploy: appDeploy,
dokployRestart: dokployRestart, dokployRestart: dokployRestart,
databaseBackup: databaseBackup, databaseBackup: databaseBackup,
userJoin: userJoin,
webhookUrl: formData.webhookUrl, webhookUrl: formData.webhookUrl,
channel: formData.channel, channel: formData.channel,
name: formData.name, name: formData.name,
notificationId: notificationId, notificationId: notificationId,
slackId: data?.slackId, slackId: data?.slackId,
dockerCleanup: dockerCleanup,
}); });
} else if (formData.type === "telegram" && data?.telegramId) { } else if (formData.type === "telegram" && data?.telegramId) {
promise = telegramMutation.mutateAsync({ promise = telegramMutation.mutateAsync({
appBuildError: appBuilderError, appBuildError: appBuildError,
appDeploy: appDeploy, appDeploy: appDeploy,
dokployRestart: dokployRestart, dokployRestart: dokployRestart,
databaseBackup: databaseBackup, databaseBackup: databaseBackup,
userJoin: userJoin,
botToken: formData.botToken, botToken: formData.botToken,
chatId: formData.chatId, chatId: formData.chatId,
name: formData.name, name: formData.name,
notificationId: notificationId, notificationId: notificationId,
telegramId: data?.telegramId, telegramId: data?.telegramId,
dockerCleanup: dockerCleanup,
}); });
} else if (formData.type === "discord" && data?.discordId) { } else if (formData.type === "discord" && data?.discordId) {
promise = discordMutation.mutateAsync({ promise = discordMutation.mutateAsync({
appBuildError: appBuilderError, appBuildError: appBuildError,
appDeploy: appDeploy, appDeploy: appDeploy,
dokployRestart: dokployRestart, dokployRestart: dokployRestart,
databaseBackup: databaseBackup, databaseBackup: databaseBackup,
userJoin: userJoin,
webhookUrl: formData.webhookUrl, webhookUrl: formData.webhookUrl,
name: formData.name, name: formData.name,
notificationId: notificationId, notificationId: notificationId,
discordId: data?.discordId, discordId: data?.discordId,
dockerCleanup: dockerCleanup,
}); });
} else if (formData.type === "email" && data?.emailId) { } else if (formData.type === "email" && data?.emailId) {
promise = emailMutation.mutateAsync({ promise = emailMutation.mutateAsync({
appBuildError: appBuilderError, appBuildError: appBuildError,
appDeploy: appDeploy, appDeploy: appDeploy,
dokployRestart: dokployRestart, dokployRestart: dokployRestart,
databaseBackup: databaseBackup, databaseBackup: databaseBackup,
userJoin: userJoin,
smtpServer: formData.smtpServer, smtpServer: formData.smtpServer,
smtpPort: formData.smtpPort, smtpPort: formData.smtpPort,
username: formData.username, username: formData.username,
@@ -198,6 +197,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
name: formData.name, name: formData.name,
notificationId: notificationId, notificationId: notificationId,
emailId: data?.emailId, emailId: data?.emailId,
dockerCleanup: dockerCleanup,
}); });
} }
@@ -554,14 +554,14 @@ export const UpdateNotification = ({ notificationId }: Props) => {
/> />
<FormField <FormField
control={form.control} control={form.control}
defaultValue={form.control._defaultValues.userJoin} defaultValue={form.control._defaultValues.appBuildError}
name="userJoin" name="appBuildError"
render={({ field }) => ( render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2"> <FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>User Join</FormLabel> <FormLabel>App Builder Error</FormLabel>
<FormDescription> <FormDescription>
Trigger the action when a user joins the app. Trigger the action when the build fails.
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>
@@ -573,6 +573,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="databaseBackup" name="databaseBackup"
@@ -596,14 +597,14 @@ export const UpdateNotification = ({ notificationId }: Props) => {
/> />
<FormField <FormField
control={form.control} control={form.control}
defaultValue={form.control._defaultValues.dokployRestart} name="dockerCleanup"
name="dokployRestart"
render={({ field }) => ( render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2"> <FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Deploy Restart</FormLabel> <FormLabel>Docker Cleanup</FormLabel>
<FormDescription> <FormDescription>
Trigger the action when a deploy is restarted. Trigger the action when the docker cleanup is
performed.
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>
@@ -617,14 +618,14 @@ export const UpdateNotification = ({ notificationId }: Props) => {
/> />
<FormField <FormField
control={form.control} control={form.control}
defaultValue={form.control._defaultValues.appBuilderError} defaultValue={form.control._defaultValues.dokployRestart}
name="appBuilderError" name="dokployRestart"
render={({ field }) => ( render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2"> <FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>App Builder Error</FormLabel> <FormLabel>Dokploy Restart</FormLabel>
<FormDescription> <FormDescription>
Trigger the action when the build fails. Trigger the action when a dokploy is restarted.
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>

View File

@@ -0,0 +1 @@
ALTER TABLE "notification" ADD COLUMN "dockerCleanup" boolean DEFAULT false NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "notification" DROP COLUMN IF EXISTS "userJoin";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -141,6 +141,20 @@
"when": 1721110706912, "when": 1721110706912,
"tag": "0019_heavy_freak", "tag": "0019_heavy_freak",
"breakpoints": true "breakpoints": true
},
{
"idx": 20,
"version": "6",
"when": 1721363861686,
"tag": "0020_fantastic_slapstick",
"breakpoints": true
},
{
"idx": 21,
"version": "6",
"when": 1721370423752,
"tag": "0021_premium_sebastian_shaw",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,18 +1,18 @@
import * as React from "react";
import { import {
Body, Body,
Button, Button,
Container, Container,
Head, Head,
Heading,
Html, Html,
Img,
Link, Link,
Preview, Preview,
Section, Section,
Text,
Tailwind, Tailwind,
Img, Text,
Heading,
} from "@react-email/components"; } from "@react-email/components";
import * as React from "react";
export type TemplateProps = { export type TemplateProps = {
projectName: string; projectName: string;
@@ -20,6 +20,7 @@ export type TemplateProps = {
applicationType: string; applicationType: string;
errorMessage: string; errorMessage: string;
buildLink: string; buildLink: string;
date: string;
}; };
export const BuildFailedEmail = ({ export const BuildFailedEmail = ({
@@ -28,6 +29,7 @@ export const BuildFailedEmail = ({
applicationType = "application", applicationType = "application",
errorMessage = "Error array.length is not a function", errorMessage = "Error array.length is not a function",
buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test", buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test",
date = "2023-05-01T00:00:00.000Z",
}: TemplateProps) => { }: TemplateProps) => {
const previewText = `Build failed for ${applicationName}`; const previewText = `Build failed for ${applicationName}`;
return ( return (
@@ -79,6 +81,9 @@ export const BuildFailedEmail = ({
<Text className="!leading-3"> <Text className="!leading-3">
Application Type: <strong>{applicationType}</strong> Application Type: <strong>{applicationType}</strong>
</Text> </Text>
<Text className="!leading-3">
Date: <strong>{date}</strong>
</Text>
</Section> </Section>
<Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2"> <Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
<Text className="!leading-3 font-bold">Reason: </Text> <Text className="!leading-3 font-bold">Reason: </Text>

View File

@@ -0,0 +1,106 @@
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Img,
Link,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";
export type TemplateProps = {
projectName: string;
applicationName: string;
applicationType: string;
buildLink: string;
date: string;
};
export const BuildSuccessEmail = ({
projectName = "dokploy",
applicationName = "frontend",
applicationType = "application",
buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test",
date = "2023-05-01T00:00:00.000Z",
}: TemplateProps) => {
const previewText = `Build success for ${applicationName}`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: {
brand: "#007291",
},
},
},
}}
>
<Body className="bg-white my-auto mx-auto font-sans px-2">
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
<Section className="mt-[32px]">
<Img
src={
"https://avatars.githubusercontent.com/u/156882017?s=200&v=4"
}
width="50"
height="50"
alt="Dokploy"
className="my-0 mx-auto"
/>
</Section>
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
Build success for <strong>{applicationName}</strong>
</Heading>
<Text className="text-black text-[14px] leading-[24px]">
Hello,
</Text>
<Text className="text-black text-[14px] leading-[24px]">
Your build for <strong>{applicationName}</strong> was successful
</Text>
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
<Text className="!leading-3 font-bold">Details: </Text>
<Text className="!leading-3">
Project Name: <strong>{projectName}</strong>
</Text>
<Text className="!leading-3">
Application Name: <strong>{applicationName}</strong>
</Text>
<Text className="!leading-3">
Application Type: <strong>{applicationType}</strong>
</Text>
<Text className="!leading-3">
Date: <strong>{date}</strong>
</Text>
</Section>
<Section className="text-center mt-[32px] mb-[32px]">
<Button
href={buildLink}
className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
>
View build
</Button>
</Section>
<Text className="text-black text-[14px] leading-[24px]">
or copy and paste this URL into your browser:{" "}
<Link href={buildLink} className="text-blue-600 no-underline">
{buildLink}
</Link>
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
};
export default BuildSuccessEmail;

View File

@@ -0,0 +1,105 @@
import {
Body,
Container,
Head,
Heading,
Html,
Img,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";
export type TemplateProps = {
projectName: string;
applicationName: string;
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
type: "error" | "success";
errorMessage?: string;
date: string;
};
export const DatabaseBackupEmail = ({
projectName = "dokploy",
applicationName = "frontend",
databaseType = "postgres",
type = "success",
errorMessage,
date = "2023-05-01T00:00:00.000Z",
}: TemplateProps) => {
const previewText = `Database backup for ${applicationName} was ${type === "success" ? "successful ✅" : "failed ❌"}`;
return (
<Html>
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: {
brand: "#007291",
},
},
},
}}
>
<Head />
<Body className="bg-white my-auto mx-auto font-sans px-2">
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
<Section className="mt-[32px]">
<Img
src={
"https://avatars.githubusercontent.com/u/156882017?s=200&v=4"
}
width="50"
height="50"
alt="Dokploy"
className="my-0 mx-auto"
/>
</Section>
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
Database backup for <strong>{applicationName}</strong>
</Heading>
<Text className="text-black text-[14px] leading-[24px]">
Hello,
</Text>
<Text className="text-black text-[14px] leading-[24px]">
Your database backup for <strong>{applicationName}</strong> was{" "}
{type === "success"
? "successful ✅"
: "failed Please check the error message below. ❌"}
.
</Text>
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
<Text className="!leading-3 font-bold">Details: </Text>
<Text className="!leading-3">
Project Name: <strong>{projectName}</strong>
</Text>
<Text className="!leading-3">
Application Name: <strong>{applicationName}</strong>
</Text>
<Text className="!leading-3">
Database Type: <strong>{databaseType}</strong>
</Text>
<Text className="!leading-3">
Date: <strong>{date}</strong>
</Text>
</Section>
{type === "error" && errorMessage ? (
<Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
<Text className="!leading-3 font-bold">Reason: </Text>
<Text className="text-[12px] leading-[24px]">
{errorMessage || "Error message not provided"}
</Text>
</Section>
) : null}
</Container>
</Body>
</Tailwind>
</Html>
);
};
export default DatabaseBackupEmail;

View File

@@ -0,0 +1,81 @@
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Img,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";
export type TemplateProps = {
message: string;
date: string;
};
export const DockerCleanupEmail = ({
message = "Docker cleanup for dokploy",
date = "2023-05-01T00:00:00.000Z",
}: TemplateProps) => {
const previewText = "Docker cleanup for dokploy";
return (
<Html>
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: {
brand: "#007291",
},
},
},
}}
>
<Head />
<Body className="bg-white my-auto mx-auto font-sans px-2">
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
<Section className="mt-[32px]">
<Img
src={
"https://avatars.githubusercontent.com/u/156882017?s=200&v=4"
}
width="50"
height="50"
alt="Dokploy"
className="my-0 mx-auto"
/>
</Section>
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
Docker cleanup for <strong>dokploy</strong>
</Heading>
<Text className="text-black text-[14px] leading-[24px]">
Hello,
</Text>
<Text className="text-black text-[14px] leading-[24px]">
The docker cleanup for <strong>dokploy</strong> was successful
</Text>
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
<Text className="!leading-3 font-bold">Details: </Text>
<Text className="!leading-3">
Message: <strong>{message}</strong>
</Text>
<Text className="!leading-3">
Date: <strong>{date}</strong>
</Text>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
};
export default DockerCleanupEmail;

View File

@@ -0,0 +1,75 @@
import {
Body,
Container,
Head,
Heading,
Html,
Img,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";
export type TemplateProps = {
date: string;
};
export const DokployRestartEmail = ({
date = "2023-05-01T00:00:00.000Z",
}: TemplateProps) => {
const previewText = "Your dokploy server was restarted";
return (
<Html>
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: {
brand: "#007291",
},
},
},
}}
>
<Head />
<Body className="bg-white my-auto mx-auto font-sans px-2">
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
<Section className="mt-[32px]">
<Img
src={
"https://avatars.githubusercontent.com/u/156882017?s=200&v=4"
}
width="50"
height="50"
alt="Dokploy"
className="my-0 mx-auto"
/>
</Section>
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
Dokploy Server Restart
</Heading>
<Text className="text-black text-[14px] leading-[24px]">
Hello,
</Text>
<Text className="text-black text-[14px] leading-[24px]">
Your dokploy server was restarted
</Text>
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
<Text className="!leading-3 font-bold">Details: </Text>
<Text className="!leading-3">
Date: <strong>{date}</strong>
</Text>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
};
export default DokployRestartEmail;

View File

@@ -23,6 +23,7 @@ import {
import { findMariadbByBackupId } from "../services/mariadb"; import { findMariadbByBackupId } from "../services/mariadb";
import { findMongoByBackupId } from "../services/mongo"; import { findMongoByBackupId } from "../services/mongo";
import { findMySqlByBackupId } from "../services/mysql"; import { findMySqlByBackupId } from "../services/mysql";
import { sendDatabaseBackupNotifications } from "../services/notification";
import { findPostgresByBackupId } from "../services/postgres"; import { findPostgresByBackupId } from "../services/postgres";
export const backupRouter = createTRPCRouter({ export const backupRouter = createTRPCRouter({
@@ -90,6 +91,7 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId); const backup = await findBackupById(input.backupId);
const postgres = await findPostgresByBackupId(backup.backupId); const postgres = await findPostgresByBackupId(backup.backupId);
await runPostgresBackup(postgres, backup); await runPostgresBackup(postgres, backup);
return true; return true;
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -35,6 +35,7 @@ import { TRPCError } from "@trpc/server";
import { scheduleJob, scheduledJobs } from "node-schedule"; import { scheduleJob, scheduledJobs } from "node-schedule";
import { appRouter } from "../root"; import { appRouter } from "../root";
import { findAdmin, updateAdmin } from "../services/admin"; import { findAdmin, updateAdmin } from "../services/admin";
import { sendDockerCleanupNotifications } from "../services/notification";
import { import {
getDokployImage, getDokployImage,
getDokployVersion, getDokployVersion,
@@ -86,6 +87,7 @@ export const settingsRouter = createTRPCRouter({
await cleanUpUnusedImages(); await cleanUpUnusedImages();
await cleanUpDockerBuilder(); await cleanUpDockerBuilder();
await cleanUpSystemPrune(); await cleanUpSystemPrune();
return true; return true;
}), }),
cleanMonitoring: adminProcedure.mutation(async () => { cleanMonitoring: adminProcedure.mutation(async () => {
@@ -144,6 +146,7 @@ export const settingsRouter = createTRPCRouter({
await cleanUpUnusedImages(); await cleanUpUnusedImages();
await cleanUpDockerBuilder(); await cleanUpDockerBuilder();
await cleanUpSystemPrune(); await cleanUpSystemPrune();
await sendDockerCleanupNotifications();
}); });
} else { } else {
const currentJob = scheduledJobs["docker-cleanup"]; const currentJob = scheduledJobs["docker-cleanup"];

View File

@@ -17,7 +17,10 @@ import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { findAdmin } from "./admin"; import { findAdmin } from "./admin";
import { createDeployment, updateDeploymentStatus } from "./deployment"; import { createDeployment, updateDeploymentStatus } from "./deployment";
import { sendBuildErrorNotifications } from "./notification"; import {
sendBuildErrorNotifications,
sendBuildSuccessNotifications,
} from "./notification";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
export type Application = typeof applications.$inferSelect; export type Application = typeof applications.$inferSelect;
@@ -157,13 +160,20 @@ export const deployApplication = async ({
} }
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done"); await updateApplicationStatus(applicationId, "done");
await sendBuildSuccessNotifications({
projectName: application.project.name,
applicationName: application.name,
applicationType: "application",
buildLink: deployment.logPath,
});
} catch (error) { } catch (error) {
console.log("Error on build", error); console.log("Error on build", error);
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error"); await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({ await sendBuildErrorNotifications({
projectName: application.project.name, projectName: application.project.name,
applicationName: application.appName, applicationName: application.name,
applicationType: "application", applicationType: "application",
errorMessage: error?.message || "Error to build", errorMessage: error?.message || "Error to build",
buildLink: deployment.logPath, buildLink: deployment.logPath,

View File

@@ -1,3 +1,8 @@
import { BuildFailedEmail } from "@/emails/emails/build-failed";
import BuildSuccessEmail from "@/emails/emails/build-success";
import DatabaseBackupEmail from "@/emails/emails/database-backup";
import DockerCleanupEmail from "@/emails/emails/docker-cleanup";
import DokployRestartEmail from "@/emails/emails/dokploy-restart";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import {
type apiCreateDiscord, type apiCreateDiscord,
@@ -20,10 +25,9 @@ import {
} from "@/server/db/schema"; } from "@/server/db/schema";
import { render } from "@react-email/components"; import { render } from "@react-email/components";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
import { and, eq, isNotNull } from "drizzle-orm";
import type SMTPTransport from "nodemailer/lib/smtp-transport"; import type SMTPTransport from "nodemailer/lib/smtp-transport";
import { BuildFailedEmail } from "@/emails/emails/build-failed";
export type Notification = typeof notifications.$inferSelect; export type Notification = typeof notifications.$inferSelect;
@@ -53,10 +57,10 @@ export const createSlackNotification = async (
slackId: newSlack.slackId, slackId: newSlack.slackId,
name: input.name, name: input.name,
appDeploy: input.appDeploy, appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "slack", notificationType: "slack",
}) })
.returning() .returning()
@@ -82,10 +86,10 @@ export const updateSlackNotification = async (
.set({ .set({
name: input.name, name: input.name,
appDeploy: input.appDeploy, appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
}) })
.where(eq(notifications.notificationId, input.notificationId)) .where(eq(notifications.notificationId, input.notificationId))
.returning() .returning()
@@ -138,10 +142,10 @@ export const createTelegramNotification = async (
telegramId: newTelegram.telegramId, telegramId: newTelegram.telegramId,
name: input.name, name: input.name,
appDeploy: input.appDeploy, appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "telegram", notificationType: "telegram",
}) })
.returning() .returning()
@@ -167,10 +171,10 @@ export const updateTelegramNotification = async (
.set({ .set({
name: input.name, name: input.name,
appDeploy: input.appDeploy, appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
}) })
.where(eq(notifications.notificationId, input.notificationId)) .where(eq(notifications.notificationId, input.notificationId))
.returning() .returning()
@@ -222,10 +226,10 @@ export const createDiscordNotification = async (
discordId: newDiscord.discordId, discordId: newDiscord.discordId,
name: input.name, name: input.name,
appDeploy: input.appDeploy, appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "discord", notificationType: "discord",
}) })
.returning() .returning()
@@ -251,10 +255,10 @@ export const updateDiscordNotification = async (
.set({ .set({
name: input.name, name: input.name,
appDeploy: input.appDeploy, appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
}) })
.where(eq(notifications.notificationId, input.notificationId)) .where(eq(notifications.notificationId, input.notificationId))
.returning() .returning()
@@ -310,10 +314,10 @@ export const createEmailNotification = async (
emailId: newEmail.emailId, emailId: newEmail.emailId,
name: input.name, name: input.name,
appDeploy: input.appDeploy, appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "email", notificationType: "email",
}) })
.returning() .returning()
@@ -339,10 +343,10 @@ export const updateEmailNotification = async (
.set({ .set({
name: input.name, name: input.name,
appDeploy: input.appDeploy, appDeploy: input.appDeploy,
userJoin: input.userJoin,
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
}) })
.where(eq(notifications.notificationId, input.notificationId)) .where(eq(notifications.notificationId, input.notificationId))
.returning() .returning()
@@ -507,69 +511,13 @@ export const sendEmailTestNotification = async (
console.log("Email notification sent successfully"); console.log("Email notification sent successfully");
}; };
export const sendBuildFailedEmail = async ({ interface BuildFailedEmailProps {
projectName,
applicationName,
applicationType,
errorMessage,
buildLink,
}: {
projectName: string; projectName: string;
applicationName: string; applicationName: string;
applicationType: string; applicationType: string;
errorMessage: string; errorMessage: string;
buildLink: string; buildLink: string;
}) => { }
const notificationList = await db.query.notifications.findMany({
where: and(
isNotNull(notifications.emailId),
eq(notifications.appBuildError, true),
),
with: {
email: true,
},
});
for (const notification of notificationList) {
const { email } = notification;
if (email) {
const {
smtpServer,
smtpPort,
username,
password,
fromAddress,
toAddresses,
} = email;
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
secure: smtpPort === 465,
auth: {
user: username,
pass: password,
},
} as SMTPTransport.Options);
const mailOptions = {
from: fromAddress,
to: toAddresses?.join(", "),
subject: "Build failed for dokploy",
html: render(
BuildFailedEmail({
projectName,
applicationName,
applicationType,
errorMessage,
buildLink,
}),
),
};
await transporter.sendMail(mailOptions);
}
}
};
// export const
export const sendBuildErrorNotifications = async ({ export const sendBuildErrorNotifications = async ({
projectName, projectName,
@@ -577,13 +525,7 @@ export const sendBuildErrorNotifications = async ({
applicationType, applicationType,
errorMessage, errorMessage,
buildLink, buildLink,
}: { }: BuildFailedEmailProps) => {
projectName: string;
applicationName: string;
applicationType: string;
errorMessage: string;
buildLink: string;
}) => {
const date = new Date(); const date = new Date();
const notificationList = await db.query.notifications.findMany({ const notificationList = await db.query.notifications.findMany({
where: eq(notifications.appBuildError, true), where: eq(notifications.appBuildError, true),
@@ -626,6 +568,7 @@ export const sendBuildErrorNotifications = async ({
applicationType, applicationType,
errorMessage, errorMessage,
buildLink, buildLink,
date: date.toLocaleString(),
}), }),
), ),
}; };
@@ -636,7 +579,7 @@ export const sendBuildErrorNotifications = async ({
const { webhookUrl } = discord; const { webhookUrl } = discord;
const embed = { const embed = {
title: "⚠️ Build Failed", title: "⚠️ Build Failed",
color: 0xff0000, // Rojo color: 0xff0000,
fields: [ fields: [
{ {
name: "Project", name: "Project",
@@ -775,3 +718,791 @@ export const sendBuildErrorNotifications = async ({
} }
} }
}; };
interface BuildSuccessEmailProps {
projectName: string;
applicationName: string;
applicationType: string;
buildLink: string;
}
export const sendBuildSuccessNotifications = async ({
projectName,
applicationName,
applicationType,
buildLink,
}: BuildSuccessEmailProps) => {
const date = new Date();
const notificationList = await db.query.notifications.findMany({
where: eq(notifications.appDeploy, true),
with: {
email: true,
discord: true,
telegram: true,
slack: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
if (email) {
const {
smtpServer,
smtpPort,
username,
password,
fromAddress,
toAddresses,
} = email;
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
secure: smtpPort === 465,
auth: {
user: username,
pass: password,
},
} as SMTPTransport.Options);
const mailOptions = {
from: fromAddress,
to: toAddresses?.join(", "),
subject: "Build success for dokploy",
html: render(
BuildSuccessEmail({
projectName,
applicationName,
applicationType,
buildLink,
date: date.toLocaleString(),
}),
),
};
await transporter.sendMail(mailOptions);
}
if (discord) {
const { webhookUrl } = discord;
const embed = {
title: "✅ Build Success",
color: 0x00ff00,
fields: [
{
name: "Project",
value: projectName,
inline: true,
},
{
name: "Application",
value: applicationName,
inline: true,
},
{
name: "Type",
value: applicationType,
inline: true,
},
{
name: "Build Link",
value: buildLink,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
};
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
embeds: [embed],
}),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
if (telegram) {
const { botToken, chatId } = telegram;
const messageText = `
<b>✅ Build Success</b>
<b>Project:</b> ${projectName}
<b>Application:</b> ${applicationName}
<b>Type:</b> ${applicationType}
<b>Time:</b> ${date.toLocaleString()}
<b>Build Details:</b> ${buildLink}
`;
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chat_id: chatId,
text: messageText,
parse_mode: "HTML",
disable_web_page_preview: true,
}),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
if (slack) {
const { webhookUrl, channel } = slack;
const message = {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Build Success*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Build Link",
value: buildLink,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: "https://doks.dev/build-details",
},
],
},
],
};
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(message),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
}
};
export const sendDatabaseBackupNotifications = async ({
projectName,
applicationName,
databaseType,
type,
errorMessage,
}: {
projectName: string;
applicationName: string;
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
type: "error" | "success";
errorMessage?: string;
}) => {
const date = new Date();
const notificationList = await db.query.notifications.findMany({
where: eq(notifications.databaseBackup, true),
with: {
email: true,
discord: true,
telegram: true,
slack: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
if (email) {
const {
smtpServer,
smtpPort,
username,
password,
fromAddress,
toAddresses,
} = email;
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
secure: smtpPort === 465,
auth: {
user: username,
pass: password,
},
} as SMTPTransport.Options);
const mailOptions = {
from: fromAddress,
to: toAddresses?.join(", "),
subject: "Database backup for dokploy",
html: render(
DatabaseBackupEmail({
projectName,
applicationName,
databaseType,
type,
errorMessage,
date: date.toLocaleString(),
}),
),
};
await transporter.sendMail(mailOptions);
}
if (discord) {
const { webhookUrl } = discord;
const embed = {
title:
type === "success"
? "✅ Database Backup Successful"
: "❌ Database Backup Failed",
color: type === "success" ? 0x00ff00 : 0xff0000,
fields: [
{
name: "Project",
value: projectName,
inline: true,
},
{
name: "Application",
value: applicationName,
inline: true,
},
{
name: "Type",
value: databaseType,
inline: true,
},
{
name: "Time",
value: date.toLocaleString(),
inline: true,
},
{
name: "Type",
value: type,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Database Backup Notification",
},
};
if (type === "error" && errorMessage) {
embed.fields.push({
name: "Error Message",
value: errorMessage as unknown as string,
});
}
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
embeds: [embed],
}),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
if (telegram) {
const { botToken, chatId } = telegram;
const statusEmoji = type === "success" ? "✅" : "❌";
const messageText = `
<b>${statusEmoji} Database Backup ${type === "success" ? "Successful" : "Failed"}</b>
<b>Project:</b> ${projectName}
<b>Application:</b> ${applicationName}
<b>Type:</b> ${databaseType}
<b>Time:</b> ${date.toLocaleString()}
<b>Status:</b> ${type === "success" ? "Successful" : "Failed"}
${type === "error" && errorMessage ? `<b>Error:</b> ${errorMessage}` : ""}
`;
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chat_id: chatId,
text: messageText,
parse_mode: "HTML",
disable_web_page_preview: true,
}),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
if (slack) {
const { webhookUrl, channel } = slack;
const message = {
channel: channel,
attachments: [
{
color: type === "success" ? "#00FF00" : "#FF0000",
pretext:
type === "success"
? ":white_check_mark: *Database Backup Successful*"
: ":x: *Database Backup Failed*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: databaseType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Type",
value: type,
},
{
title: "Status",
value: type === "success" ? "Successful" : "Failed",
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: "https://doks.dev/build-details",
},
],
},
],
};
if (type === "error" && errorMessage) {
message.attachments[0].fields.push({
title: "Error Message",
value: errorMessage,
});
}
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(message),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
}
};
export const sendDockerCleanupNotifications = async (
message = "Docker cleanup for dokploy",
) => {
const date = new Date();
const notificationList = await db.query.notifications.findMany({
where: eq(notifications.dockerCleanup, true),
with: {
email: true,
discord: true,
telegram: true,
slack: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
if (email) {
const {
smtpServer,
smtpPort,
username,
password,
fromAddress,
toAddresses,
} = email;
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
secure: smtpPort === 465,
auth: {
user: username,
pass: password,
},
} as SMTPTransport.Options);
const mailOptions = {
from: fromAddress,
to: toAddresses?.join(", "),
subject: "Docker cleanup for dokploy",
html: render(
DockerCleanupEmail({
message,
date: date.toLocaleString(),
}),
),
};
await transporter.sendMail(mailOptions);
}
if (discord) {
const { webhookUrl } = discord;
const embed = {
title: "✅ Docker Cleanup",
color: 0x00ff00,
fields: [
{
name: "Message",
value: message,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Docker Cleanup Notification",
},
};
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
embeds: [embed],
}),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
if (telegram) {
const { botToken, chatId } = telegram;
const messageText = `
<b>✅ Docker Cleanup</b>
<b>Message:</b> ${message}
<b>Time:</b> ${date.toLocaleString()}
`;
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chat_id: chatId,
text: messageText,
parse_mode: "HTML",
disable_web_page_preview: true,
}),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
if (slack) {
const { webhookUrl, channel } = slack;
const messageResponse = {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Docker Cleanup*",
fields: [
{
title: "Message",
value: message,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: "https://doks.dev/build-details",
},
],
},
],
};
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(messageResponse),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
}
};
export const sendEmailNotification = async (
connection: typeof email.$inferSelect,
subject: string,
htmlContent: string,
) => {
const { smtpServer, smtpPort, username, password, fromAddress, toAddresses } =
connection;
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
secure: smtpPort === 465,
auth: { user: username, pass: password },
});
await transporter.sendMail({
from: fromAddress,
to: toAddresses.join(", "),
subject,
html: htmlContent,
});
};
export const sendDiscordNotification = async (
connection: typeof discord.$inferSelect,
embed: any,
) => {
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");
};
export const sendTelegramNotification = async (
connection: typeof telegram.$inferSelect,
messageText: string,
) => {
const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: connection.chatId,
text: messageText,
parse_mode: "HTML",
disable_web_page_preview: true,
}),
});
if (!response.ok) throw new Error("Failed to send Telegram notification");
};
export const sendSlackNotification = async (
connection: typeof slack.$inferSelect,
message: any,
) => {
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");
};
export const sendDokployRestartNotifications = async () => {
const date = new Date();
const notificationList = await db.query.notifications.findMany({
where: eq(notifications.dokployRestart, true),
with: {
email: true,
discord: true,
telegram: true,
slack: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
if (email) {
const {
smtpServer,
smtpPort,
username,
password,
fromAddress,
toAddresses,
} = email;
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
secure: smtpPort === 465,
auth: {
user: username,
pass: password,
},
} as SMTPTransport.Options);
const mailOptions = {
from: fromAddress,
to: toAddresses?.join(", "),
subject: "Dokploy Server Restarted",
html: render(
DokployRestartEmail({
date: date.toLocaleString(),
}),
),
};
await transporter.sendMail(mailOptions);
}
if (discord) {
const { webhookUrl } = discord;
const embed = {
title: "✅ Dokploy Server Restarted",
color: 0xff0000,
fields: [
{
name: "Time",
value: date.toLocaleString(),
inline: true,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Restart Notification",
},
};
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
embeds: [embed],
}),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
if (telegram) {
const { botToken, chatId } = telegram;
const messageText = `
<b>✅ Dokploy Serverd Restarted</b>
<b>Time:</b> ${date.toLocaleString()}
`;
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chat_id: chatId,
text: messageText,
parse_mode: "HTML",
disable_web_page_preview: true,
}),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
if (slack) {
const { webhookUrl, channel } = slack;
const message = {
channel: channel,
attachments: [
{
color: "#FF0000",
pretext: ":white_check_mark: *Dokploy Server Restarted*",
fields: [
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: "https://doks.dev/build-details",
},
],
},
],
};
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(message),
});
if (!response.ok) {
throw new Error("Error to send test notification");
}
}
}
};

View File

@@ -1,8 +1,8 @@
import { nanoid } from "nanoid";
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { z } from "zod"; import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
export const notificationType = pgEnum("notificationType", [ export const notificationType = pgEnum("notificationType", [
"slack", "slack",
@@ -18,10 +18,10 @@ export const notifications = pgTable("notification", {
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
name: text("name").notNull(), name: text("name").notNull(),
appDeploy: boolean("appDeploy").notNull().default(false), appDeploy: boolean("appDeploy").notNull().default(false),
userJoin: boolean("userJoin").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),
notificationType: notificationType("notificationType").notNull(), notificationType: notificationType("notificationType").notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -107,7 +107,7 @@ export const apiCreateSlack = notificationsSchema
dokployRestart: true, dokployRestart: true,
name: true, name: true,
appDeploy: true, appDeploy: true,
userJoin: true, dockerCleanup: true,
}) })
.extend({ .extend({
webhookUrl: z.string().min(1), webhookUrl: z.string().min(1),
@@ -132,7 +132,7 @@ export const apiCreateTelegram = notificationsSchema
dokployRestart: true, dokployRestart: true,
name: true, name: true,
appDeploy: true, appDeploy: true,
userJoin: true, dockerCleanup: true,
}) })
.extend({ .extend({
botToken: z.string().min(1), botToken: z.string().min(1),
@@ -157,7 +157,7 @@ export const apiCreateDiscord = notificationsSchema
dokployRestart: true, dokployRestart: true,
name: true, name: true,
appDeploy: true, appDeploy: true,
userJoin: true, dockerCleanup: true,
}) })
.extend({ .extend({
webhookUrl: z.string().min(1), webhookUrl: z.string().min(1),
@@ -180,7 +180,7 @@ export const apiCreateEmail = notificationsSchema
dokployRestart: true, dokployRestart: true,
name: true, name: true,
appDeploy: true, appDeploy: true,
userJoin: true, dockerCleanup: true,
}) })
.extend({ .extend({
smtpServer: z.string().min(1), smtpServer: z.string().min(1),

View File

@@ -2,6 +2,7 @@ import http from "node:http";
import { migration } from "@/server/db/migration"; import { migration } from "@/server/db/migration";
import { config } from "dotenv"; import { config } from "dotenv";
import next from "next"; import next from "next";
import { sendDokployRestartNotifications } from "./api/services/notification";
import { deploymentWorker } from "./queues/deployments-queue"; import { deploymentWorker } from "./queues/deployments-queue";
import { setupDirectories } from "./setup/config-paths"; import { setupDirectories } from "./setup/config-paths";
import { initializePostgres } from "./setup/postgres-setup"; import { initializePostgres } from "./setup/postgres-setup";
@@ -57,6 +58,8 @@ void app.prepare().then(async () => {
await new Promise((resolve) => setTimeout(resolve, 7000)); await new Promise((resolve) => setTimeout(resolve, 7000));
await migration(); await migration();
} }
await sendDokployRestartNotifications();
server.listen(PORT); server.listen(PORT);
console.log("Server Started:", PORT); console.log("Server Started:", PORT);
deploymentWorker.run(); deploymentWorker.run();

View File

@@ -2,6 +2,8 @@ import { unlink } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { BackupSchedule } from "@/server/api/services/backup"; import type { BackupSchedule } from "@/server/api/services/backup";
import type { Mariadb } from "@/server/api/services/mariadb"; import type { Mariadb } from "@/server/api/services/mariadb";
import { sendDatabaseBackupNotifications } from "@/server/api/services/notification";
import { findProjectById } from "@/server/api/services/project";
import { getServiceContainer } from "../docker/utils"; import { getServiceContainer } from "../docker/utils";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
import { uploadToS3 } from "./utils"; import { uploadToS3 } from "./utils";
@@ -10,7 +12,8 @@ export const runMariadbBackup = async (
mariadb: Mariadb, mariadb: Mariadb,
backup: BackupSchedule, backup: BackupSchedule,
) => { ) => {
const { appName, databasePassword, databaseUser } = mariadb; const { appName, databasePassword, databaseUser, projectId, name } = mariadb;
const project = await findProjectById(projectId);
const { prefix, database } = backup; const { prefix, database } = backup;
const destination = backup.destination; const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`; const backupFileName = `${new Date().toISOString()}.sql.gz`;
@@ -31,8 +34,22 @@ export const runMariadbBackup = async (
`docker cp ${containerId}:/backup/${backupFileName} ${hostPath}`, `docker cp ${containerId}:/backup/${backupFileName} ${hostPath}`,
); );
await uploadToS3(destination, bucketDestination, hostPath); await uploadToS3(destination, bucketDestination, hostPath);
await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "mariadb",
type: "success",
});
} catch (error) { } catch (error) {
console.log(error); console.log(error);
await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "mariadb",
type: "error",
errorMessage: error?.message || "Error message not provided",
});
throw error; throw error;
} finally { } finally {
await unlink(hostPath); await unlink(hostPath);

View File

@@ -2,13 +2,16 @@ import { unlink } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { BackupSchedule } from "@/server/api/services/backup"; import type { BackupSchedule } from "@/server/api/services/backup";
import type { Mongo } from "@/server/api/services/mongo"; import type { Mongo } from "@/server/api/services/mongo";
import { sendDatabaseBackupNotifications } from "@/server/api/services/notification";
import { findProjectById } from "@/server/api/services/project";
import { getServiceContainer } from "../docker/utils"; import { getServiceContainer } from "../docker/utils";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
import { uploadToS3 } from "./utils"; import { uploadToS3 } from "./utils";
// mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true // mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const { appName, databasePassword, databaseUser } = mongo; const { appName, databasePassword, databaseUser, projectId, name } = mongo;
const project = await findProjectById(projectId);
const { prefix, database } = backup; const { prefix, database } = backup;
const destination = backup.destination; const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.dump.gz`; const backupFileName = `${new Date().toISOString()}.dump.gz`;
@@ -27,8 +30,22 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
); );
await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`); await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`);
await uploadToS3(destination, bucketDestination, hostPath); await uploadToS3(destination, bucketDestination, hostPath);
await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "mongodb",
type: "success",
});
} catch (error) { } catch (error) {
console.log(error); console.log(error);
await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "mongodb",
type: "error",
errorMessage: error?.message || "Error message not provided",
});
throw error; throw error;
} finally { } finally {
await unlink(hostPath); await unlink(hostPath);

View File

@@ -2,12 +2,15 @@ import { unlink } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { BackupSchedule } from "@/server/api/services/backup"; import type { BackupSchedule } from "@/server/api/services/backup";
import type { MySql } from "@/server/api/services/mysql"; import type { MySql } from "@/server/api/services/mysql";
import { sendDatabaseBackupNotifications } from "@/server/api/services/notification";
import { findProjectById } from "@/server/api/services/project";
import { getServiceContainer } from "../docker/utils"; import { getServiceContainer } from "../docker/utils";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
import { uploadToS3 } from "./utils"; import { uploadToS3 } from "./utils";
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { appName, databaseRootPassword } = mysql; const { appName, databaseRootPassword, projectId, name } = mysql;
const project = await findProjectById(projectId);
const { prefix, database } = backup; const { prefix, database } = backup;
const destination = backup.destination; const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`; const backupFileName = `${new Date().toISOString()}.sql.gz`;
@@ -29,8 +32,21 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
`docker cp ${containerId}:/backup/${backupFileName} ${hostPath}`, `docker cp ${containerId}:/backup/${backupFileName} ${hostPath}`,
); );
await uploadToS3(destination, bucketDestination, hostPath); await uploadToS3(destination, bucketDestination, hostPath);
await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "mysql",
type: "success",
});
} catch (error) { } catch (error) {
console.log(error); console.log(error);
await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "mysql",
type: "error",
errorMessage: error?.message || "Error message not provided",
});
throw error; throw error;
} finally { } finally {
await unlink(hostPath); await unlink(hostPath);

View File

@@ -1,7 +1,9 @@
import { unlink } from "node:fs/promises"; import { unlink } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { BackupSchedule } from "@/server/api/services/backup"; import type { BackupSchedule } from "@/server/api/services/backup";
import { sendDatabaseBackupNotifications } from "@/server/api/services/notification";
import type { Postgres } from "@/server/api/services/postgres"; import type { Postgres } from "@/server/api/services/postgres";
import { findProjectById } from "@/server/api/services/project";
import { getServiceContainer } from "../docker/utils"; import { getServiceContainer } from "../docker/utils";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
import { uploadToS3 } from "./utils"; import { uploadToS3 } from "./utils";
@@ -10,7 +12,9 @@ export const runPostgresBackup = async (
postgres: Postgres, postgres: Postgres,
backup: BackupSchedule, backup: BackupSchedule,
) => { ) => {
const { appName, databaseUser } = postgres; const { appName, databaseUser, name, projectId } = postgres;
const project = await findProjectById(projectId);
const { prefix, database } = backup; const { prefix, database } = backup;
const destination = backup.destination; const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`; const backupFileName = `${new Date().toISOString()}.sql.gz`;
@@ -29,8 +33,21 @@ export const runPostgresBackup = async (
await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`); await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`);
await uploadToS3(destination, bucketDestination, hostPath); await uploadToS3(destination, bucketDestination, hostPath);
await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "postgres",
type: "success",
});
} catch (error) { } catch (error) {
console.log(error); await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "postgres",
type: "error",
errorMessage: error?.message || "Error message not provided",
});
throw error; throw error;
} finally { } finally {
await unlink(hostPath); await unlink(hostPath);