feat: implement test connection for all the providers

This commit is contained in:
Mauricio Siu
2024-07-12 01:29:24 -06:00
parent 342ff4b589
commit 5fadd73732
12 changed files with 6308 additions and 191 deletions

View File

@@ -19,10 +19,9 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { 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 { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
@@ -72,6 +71,7 @@ const mySchema = z.discriminatedUnion("type", [
smtpPort: z.string().min(1), smtpPort: z.string().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),
toAddresses: z.array(z.string()).min(1), toAddresses: z.array(z.string()).min(1),
}) })
.merge(baseDatabaseSchema), .merge(baseDatabaseSchema),
@@ -101,6 +101,8 @@ type AddNotification = z.infer<typeof mySchema>;
export const AddNotification = () => { export const AddNotification = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
api.notification.testConnection.useMutation();
const slackMutation = api.notification.createSlack.useMutation(); const slackMutation = api.notification.createSlack.useMutation();
const telegramMutation = api.notification.createTelegram.useMutation(); const telegramMutation = api.notification.createTelegram.useMutation();
const discordMutation = api.notification.createDiscord.useMutation(); const discordMutation = api.notification.createDiscord.useMutation();
@@ -114,11 +116,23 @@ export const AddNotification = () => {
}, },
resolver: zodResolver(mySchema), resolver: zodResolver(mySchema),
}); });
const type = form.watch("type");
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "toAddresses",
});
useEffect(() => {
if (type === "email") {
append("");
}
}, [type, append]);
useEffect(() => { useEffect(() => {
form.reset(); form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]); }, [form, form.reset, form.formState.isSubmitSuccessful]);
const type = form.watch("type");
const activeMutation = { const activeMutation = {
slack: slackMutation, slack: slackMutation,
telegram: telegramMutation, telegram: telegramMutation,
@@ -178,6 +192,7 @@ export const AddNotification = () => {
smtpPort: data.smtpPort, smtpPort: data.smtpPort,
username: data.username, username: data.username,
password: data.password, password: data.password,
fromAddress: data.fromAddress,
toAddresses: data.toAddresses, toAddresses: data.toAddresses,
name: data.name, name: data.name,
}); });
@@ -339,7 +354,7 @@ export const AddNotification = () => {
<FormLabel>Bot Token</FormLabel> <FormLabel>Bot Token</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ" placeholder="6660491268:AAFMGmajZOVewpMNZCgJr5H7cpXpoZPgvXw"
{...field} {...field}
/> />
</FormControl> </FormControl>
@@ -356,7 +371,7 @@ export const AddNotification = () => {
<FormItem> <FormItem>
<FormLabel>Chat ID</FormLabel> <FormLabel>Chat ID</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Chat ID" {...field} /> <Input placeholder="431231869" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -452,8 +467,21 @@ export const AddNotification = () => {
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control}
name="fromAddress"
render={({ field }) => (
<FormItem>
<FormLabel>From Address</FormLabel>
<FormControl>
<Input placeholder="from@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control} control={form.control}
name="toAddresses" name="toAddresses"
render={({ field }) => ( render={({ field }) => (
@@ -466,7 +494,54 @@ export const AddNotification = () => {
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> /> */}
<div className="flex flex-col gap-2 pt-2">
<FormLabel>To Addresses</FormLabel>
{fields.map((field, index) => (
<div
key={field.id}
className="flex flex-row gap-2 w-full"
>
<FormField
control={form.control}
name={`toAddresses.${index}`}
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<Input
placeholder="email@example.com"
className="w-full"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
variant="outline"
type="button"
onClick={() => {
remove(index);
}}
>
Remove
</Button>
</div>
))}
</div>
<Button
variant="outline"
type="button"
onClick={() => {
append("");
}}
>
Add
</Button>
</> </>
)} )}
</div> </div>
@@ -561,7 +636,35 @@ export const AddNotification = () => {
</div> </div>
</form> </form>
<DialogFooter> <DialogFooter className="flex flex-row gap-2 !justify-between w-full">
<Button
isLoading={isLoadingConnection}
variant="secondary"
onClick={async () => {
await testConnection({
webhookUrl: form.getValues("webhookUrl"),
channel: form.getValues("channel"),
notificationType: type,
botToken: form.getValues("botToken"),
chatId: form.getValues("chatId"),
//
smtpPort: form.getValues("smtpPort"),
smtpServer: form.getValues("smtpServer"),
username: form.getValues("username"),
password: form.getValues("password"),
toAddresses: form.getValues("toAddresses"),
fromAddress: form.getValues("fromAddress"),
})
.then(async () => {
toast.success("Connection Success");
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Send Test
</Button>
<Button <Button
isLoading={form.formState.isSubmitting} isLoading={form.formState.isSubmitting}
form="hook-form" form="hook-form"

View File

@@ -0,0 +1,7 @@
DO $$ BEGIN
CREATE TYPE "public"."notificationType" AS ENUM('slack', 'telegram', 'discord', 'email');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "notificationType" "notificationType" NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "email" ADD COLUMN "fromAddress" text NOT NULL;

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": 1720507012671, "when": 1720507012671,
"tag": "0019_careless_hardball", "tag": "0019_careless_hardball",
"breakpoints": true "breakpoints": true
},
{
"idx": 20,
"version": "6",
"when": 1720762222675,
"tag": "0020_ambitious_edwin_jarvis",
"breakpoints": true
},
{
"idx": 21,
"version": "6",
"when": 1720768664067,
"tag": "0021_nervous_dragon_lord",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,159 +1,161 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.3.1", "version": "v0.3.1",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "npm run build-server && npm run build-next", "build": "npm run build-server && npm run build-next",
"start": "node dist/server.mjs", "start": "node dist/server.mjs",
"build-server": "tsx esbuild.config.ts", "build-server": "tsx esbuild.config.ts",
"build-next": "next build", "build-next": "next build",
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run", "setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
"reset-password": "node dist/reset-password.mjs", "reset-password": "node dist/reset-password.mjs",
"dev": "tsx watch -r dotenv/config ./server/server.ts --project tsconfig.server.json ", "dev": "tsx watch -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts", "studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts", "migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
"migration:run": "tsx -r dotenv/config migration.ts", "migration:run": "tsx -r dotenv/config migration.ts",
"migration:up": "drizzle-kit up --config ./server/db/drizzle.config.ts", "migration:up": "drizzle-kit up --config ./server/db/drizzle.config.ts",
"migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts", "migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts",
"db:push": "drizzle-kit --config ./server/db/drizzle.config.ts", "db:push": "drizzle-kit --config ./server/db/drizzle.config.ts",
"db:truncate": "tsx -r dotenv/config ./server/db/reset.ts", "db:truncate": "tsx -r dotenv/config ./server/db/reset.ts",
"db:studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts", "db:studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"lint": "biome lint", "lint": "biome lint",
"db:seed": "tsx -r dotenv/config ./server/db/seed.ts", "db:seed": "tsx -r dotenv/config ./server/db/seed.ts",
"db:clean": "tsx -r dotenv/config ./server/db/reset.ts", "db:clean": "tsx -r dotenv/config ./server/db/reset.ts",
"docker:build": "./docker/build.sh", "docker:build": "./docker/build.sh",
"docker:push": "./docker/push.sh", "docker:push": "./docker/push.sh",
"docker:build:canary": "./docker/build.sh canary", "docker:build:canary": "./docker/build.sh canary",
"docker:push:canary": "./docker/push.sh canary", "docker:push:canary": "./docker/push.sh canary",
"version": "echo $(node -p \"require('./package.json').version\")", "version": "echo $(node -p \"require('./package.json').version\")",
"test": "vitest --config __test__/vitest.config.ts" "test": "vitest --config __test__/vitest.config.ts"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.515.0", "@aws-sdk/client-s3": "3.515.0",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1", "@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1", "@codemirror/language": "^6.10.1",
"@codemirror/legacy-modes": "6.4.0", "@codemirror/legacy-modes": "6.4.0",
"@dokploy/trpc-openapi": "0.0.4", "@dokploy/trpc-openapi": "0.0.4",
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@lucia-auth/adapter-drizzle": "1.0.7", "@lucia-auth/adapter-drizzle": "1.0.7",
"@octokit/auth-app": "^6.0.4", "@octokit/auth-app": "^6.0.4",
"@octokit/webhooks": "^13.2.7", "@octokit/webhooks": "^13.2.7",
"@radix-ui/react-accordion": "1.1.2", "@radix-ui/react-accordion": "1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.16.0", "@tanstack/react-table": "^8.16.0",
"@trpc/client": "^10.43.6", "@trpc/client": "^10.43.6",
"@trpc/next": "^10.43.6", "@trpc/next": "^10.43.6",
"@trpc/react-query": "^10.43.6", "@trpc/react-query": "^10.43.6",
"@trpc/server": "^10.43.6", "@trpc/server": "^10.43.6",
"@uiw/codemirror-theme-github": "^4.22.1", "@uiw/codemirror-theme-github": "^4.22.1",
"@uiw/react-codemirror": "^4.22.1", "@uiw/react-codemirror": "^4.22.1",
"@xterm/addon-attach": "0.10.0", "@xterm/addon-attach": "0.10.0",
"@xterm/xterm": "^5.4.0", "@xterm/xterm": "^5.4.0",
"bcrypt": "5.1.1", "bcrypt": "5.1.1",
"bl": "6.0.11", "bl": "6.0.11",
"boxen": "^7.1.1", "boxen": "^7.1.1",
"bullmq": "5.4.2", "bullmq": "5.4.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"cmdk": "^0.2.0", "cmdk": "^0.2.0",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"copy-webpack-plugin": "^12.0.2", "copy-webpack-plugin": "^12.0.2",
"date-fns": "3.6.0", "date-fns": "3.6.0",
"dockerode": "4.0.2", "dockerode": "4.0.2",
"dockerode-compose": "^1.4.0", "dockerode-compose": "^1.4.0",
"dockerstats": "2.4.2", "dockerstats": "2.4.2",
"dotenv": "16.4.5", "dotenv": "16.4.5",
"drizzle-orm": "^0.30.8", "drizzle-orm": "^0.30.8",
"drizzle-zod": "0.5.1", "drizzle-zod": "0.5.1",
"hi-base32": "^0.5.1", "hi-base32": "^0.5.1",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"k6": "^0.0.0", "k6": "^0.0.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"lucia": "^3.0.1", "lucia": "^3.0.1",
"lucide-react": "^0.312.0", "lucide-react": "^0.312.0",
"nanoid": "3", "nanoid": "3",
"next": "^14.1.3", "next": "^14.1.3",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"node-os-utils": "1.3.7", "node-os-utils": "1.3.7",
"node-pty": "1.0.0", "node-pty": "1.0.0",
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"octokit": "3.1.2", "octokit": "3.1.2",
"otpauth": "^9.2.3", "otpauth": "^9.2.3",
"postgres": "3.4.4", "postgres": "3.4.4",
"public-ip": "6.0.2", "public-ip": "6.0.2",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.49.3", "react-hook-form": "^7.49.3",
"recharts": "^2.12.3", "recharts": "^2.12.3",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"sonner": "^1.4.0", "sonner": "^1.4.0",
"superjson": "^2.2.1", "superjson": "^2.2.1",
"swagger-ui-react": "^5.17.14", "swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tar-fs": "3.0.5", "tar-fs": "3.0.5",
"use-resize-observer": "9.1.0", "use-resize-observer": "9.1.0",
"ws": "8.16.0", "ws": "8.16.0",
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
"zod": "^3.23.4" "zod": "^3.23.4",
}, "nodemailer": "6.9.14"
"devDependencies": { },
"@biomejs/biome": "1.7.1", "devDependencies": {
"@types/bcrypt": "5.0.2", "@biomejs/biome": "1.7.1",
"@types/dockerode": "3.3.23", "@types/bcrypt": "5.0.2",
"@types/js-yaml": "4.0.9", "@types/dockerode": "3.3.23",
"@types/lodash": "4.17.4", "@types/js-yaml": "4.0.9",
"@types/node": "^18.17.0", "@types/lodash": "4.17.4",
"@types/node-os-utils": "1.3.4", "@types/node": "^18.17.0",
"@types/node-schedule": "2.1.6", "@types/node-os-utils": "1.3.4",
"@types/qrcode": "^1.5.5", "@types/node-schedule": "2.1.6",
"@types/react": "^18.2.37", "@types/qrcode": "^1.5.5",
"@types/react-dom": "^18.2.15", "@types/react": "^18.2.37",
"@types/swagger-ui-react": "^4.18.3", "@types/react-dom": "^18.2.15",
"@types/tar-fs": "2.0.4", "@types/swagger-ui-react": "^4.18.3",
"@types/ws": "8.5.10", "@types/tar-fs": "2.0.4",
"autoprefixer": "^10.4.14", "@types/ws": "8.5.10",
"drizzle-kit": "^0.21.1", "autoprefixer": "^10.4.14",
"esbuild": "0.20.2", "drizzle-kit": "^0.21.1",
"localtunnel": "2.0.2", "esbuild": "0.20.2",
"postcss": "^8.4.31", "localtunnel": "2.0.2",
"prettier": "^3.2.4", "postcss": "^8.4.31",
"prettier-plugin-tailwindcss": "^0.5.11", "prettier": "^3.2.4",
"tailwindcss": "^3.4.1", "prettier-plugin-tailwindcss": "^0.5.11",
"tsx": "^4.7.0", "tailwindcss": "^3.4.1",
"typescript": "^5.4.2", "tsx": "^4.7.0",
"vite-tsconfig-paths": "4.3.2", "typescript": "^5.4.2",
"vitest": "^1.6.0", "vite-tsconfig-paths": "4.3.2",
"xterm-readline": "1.1.1" "vitest": "^1.6.0",
}, "xterm-readline": "1.1.1",
"ct3aMetadata": { "@types/nodemailer": "^6.4.15"
"initVersion": "7.25.2" },
}, "ct3aMetadata": {
"engines": { "initVersion": "7.25.2"
"node": "^18.18.0", },
"pnpm": ">=8.15.4" "engines": {
} "node": "^18.18.0",
"pnpm": ">=8.15.4"
}
} }

17
pnpm-lock.yaml generated
View File

@@ -209,6 +209,9 @@ dependencies:
node-schedule: node-schedule:
specifier: 2.1.1 specifier: 2.1.1
version: 2.1.1 version: 2.1.1
nodemailer:
specifier: 6.9.14
version: 6.9.14
octokit: octokit:
specifier: 3.1.2 specifier: 3.1.2
version: 3.1.2 version: 3.1.2
@@ -295,6 +298,9 @@ devDependencies:
'@types/node-schedule': '@types/node-schedule':
specifier: 2.1.6 specifier: 2.1.6
version: 2.1.6 version: 2.1.6
'@types/nodemailer':
specifier: ^6.4.15
version: 6.4.15
'@types/qrcode': '@types/qrcode':
specifier: ^1.5.5 specifier: ^1.5.5
version: 1.5.5 version: 1.5.5
@@ -5210,6 +5216,12 @@ packages:
undici-types: 5.26.5 undici-types: 5.26.5
dev: false dev: false
/@types/nodemailer@6.4.15:
resolution: {integrity: sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==}
dependencies:
'@types/node': 18.19.24
dev: true
/@types/prop-types@15.7.11: /@types/prop-types@15.7.11:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
@@ -8306,6 +8318,11 @@ packages:
sorted-array-functions: 1.3.0 sorted-array-functions: 1.3.0
dev: false dev: false
/nodemailer@6.9.14:
resolution: {integrity: sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==}
engines: {node: '>=6.0.0'}
dev: false
/nopt@5.0.0: /nopt@5.0.0:
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
engines: {node: '>=6'} engines: {node: '>=6'}

View File

@@ -5,15 +5,14 @@ import {
} from "@/server/api/trpc"; } from "@/server/api/trpc";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import {
apiCreateDestination,
apiCreateDiscord, apiCreateDiscord,
apiCreateEmail, apiCreateEmail,
apiCreateSlack, apiCreateSlack,
apiCreateTelegram, apiCreateTelegram,
apiFindOneNotification, apiFindOneNotification,
apiSendTest,
apiUpdateDestination, apiUpdateDestination,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { HeadBucketCommand, S3Client } from "@aws-sdk/client-s3";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { updateDestinationById } from "../services/destination"; import { updateDestinationById } from "../services/destination";
import { import {
@@ -24,6 +23,7 @@ import {
findNotificationById, findNotificationById,
removeNotificationById, removeNotificationById,
} from "../services/notification"; } from "../services/notification";
import nodemailer from "nodemailer";
export const notificationRouter = createTRPCRouter({ export const notificationRouter = createTRPCRouter({
createSlack: adminProcedure createSlack: adminProcedure
@@ -32,6 +32,7 @@ export const notificationRouter = createTRPCRouter({
try { try {
return await createSlackNotification(input); return await createSlackNotification(input);
} catch (error) { } catch (error) {
console.log(error);
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Error to create the destination", message: "Error to create the destination",
@@ -97,30 +98,123 @@ export const notificationRouter = createTRPCRouter({
return notification; return notification;
}), }),
testConnection: adminProcedure testConnection: adminProcedure
.input(apiCreateDestination) .input(apiSendTest)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { secretAccessKey, bucket, region, endpoint, accessKey } = input; const notificationType = input.notificationType;
const s3Client = new S3Client({ console.log(input);
region: region,
...(endpoint && {
endpoint: endpoint,
}),
credentials: {
accessKeyId: accessKey,
secretAccessKey: secretAccessKey,
},
forcePathStyle: true,
});
const headBucketCommand = new HeadBucketCommand({ Bucket: bucket });
try { if (notificationType === "slack") {
await s3Client.send(headBucketCommand); // go to your slack dashboard
} catch (error) { // go to integrations
throw new TRPCError({ // add a new integration
code: "BAD_REQUEST", // select incoming webhook
message: "Error to connect to bucket", // copy the webhook url
cause: error, console.log("test slack");
}); const { webhookUrl, channel } = input;
try {
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text: "Test notification", channel }),
});
} catch (err) {
console.log(err);
}
} else if (notificationType === "telegram") {
// start telegram
// search BotFather
// send /newbot
// name
// name-with-bot-at-the-end
// copy the token
// search @userinfobot
// send /start
// copy the Id
const { botToken, chatId } = input;
try {
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: "Test notification",
}),
});
if (!response.ok) {
throw new Error(
`Error sending Telegram notification: ${response.statusText}`,
);
}
console.log("Telegram notification sent successfully");
} catch (error) {
console.error("Error sending Telegram notification:", error);
throw new Error("Error sending Telegram notification");
}
} else if (notificationType === "discord") {
const { webhookUrl } = input;
try {
// go to your discord server
// go to settings
// go to integrations
// add a new integration
// select webhook
// copy the webhook url
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: "Test notification",
}),
});
if (!response.ok) {
throw new Error(
`Error sending Discord notification: ${response.statusText}`,
);
}
console.log("Discord notification sent successfully");
} catch (error) {
console.error("Error sending Discord notification:", error);
throw new Error("Error sending Discord notification");
}
} else if (notificationType === "email") {
const { smtpServer, smtpPort, username, password, toAddresses } = input;
try {
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
secure: smtpPort === "465",
auth: {
user: username,
pass: password,
},
});
// need to add a valid from address
const fromAddress = "no-reply@emails.dokploy.com";
const mailOptions = {
from: fromAddress,
to: toAddresses?.join(", "),
subject: "Test email",
text: "Test email",
};
await transporter.sendMail(mailOptions);
console.log("Email notification sent successfully");
} catch (error) {
console.error("Error sending Email notification:", error);
throw new Error("Error sending Email notification");
}
} }
}), }),

View File

@@ -45,6 +45,7 @@ export const createSlackNotification = async (
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
notificationType: "slack",
}) })
.returning() .returning()
.then((value) => value[0]); .then((value) => value[0]);
@@ -90,6 +91,7 @@ export const createTelegramNotification = async (
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
notificationType: "telegram",
}) })
.returning() .returning()
.then((value) => value[0]); .then((value) => value[0]);
@@ -134,6 +136,7 @@ export const createDiscordNotification = async (
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
notificationType: "discord",
}) })
.returning() .returning()
.then((value) => value[0]); .then((value) => value[0]);
@@ -182,6 +185,7 @@ export const createEmailNotification = async (
appBuildError: input.appBuildError, appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup, databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart, dokployRestart: input.dokployRestart,
notificationType: "email",
}) })
.returning() .returning()
.then((value) => value[0]); .then((value) => value[0]);
@@ -239,3 +243,22 @@ export const updateDestinationById = async (
return result[0]; return result[0];
}; };
export const sendNotification = async (
notificationData: Partial<Notification>,
) => {
// if(notificationData.notificationType === "slack"){
// const { webhookUrl, channel } = notificationData;
// try {
// const response = await fetch(webhookUrl, {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify({ text: "Test notification", channel }),
// });
// } catch (err) {
// console.log(err);
// }
// }
};

View File

@@ -20,7 +20,6 @@ import {
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
import { registry } from "./registry"; import { registry } from "./registry";
import { generatePassword } from "@/templates/utils";
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]); export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);

View File

@@ -1,9 +1,16 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { boolean, pgTable, text } from "drizzle-orm/pg-core"; import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
export const notificationType = pgEnum("notificationType", [
"slack",
"telegram",
"discord",
"email",
]);
export const notifications = pgTable("notification", { export const notifications = pgTable("notification", {
notificationId: text("notificationId") notificationId: text("notificationId")
.notNull() .notNull()
@@ -15,6 +22,7 @@ export const notifications = pgTable("notification", {
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),
notificationType: notificationType("notificationType").notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
@@ -67,6 +75,7 @@ export const email = pgTable("email", {
smtpPort: text("smtpPort").notNull(), smtpPort: text("smtpPort").notNull(),
username: text("username").notNull(), username: text("username").notNull(),
password: text("password").notNull(), password: text("password").notNull(),
fromAddress: text("fromAddress").notNull(),
toAddresses: text("toAddress").array().notNull(), toAddresses: text("toAddress").array().notNull(),
}); });
@@ -149,6 +158,7 @@ export const apiCreateEmail = notificationsSchema
smtpPort: z.string().min(1), smtpPort: z.string().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),
toAddresses: z.array(z.string()).min(1), toAddresses: z.array(z.string()).min(1),
}) })
.required(); .required();
@@ -158,3 +168,18 @@ export const apiFindOneNotification = notificationsSchema
notificationId: true, notificationId: true,
}) })
.required(); .required();
export const apiSendTest = notificationsSchema
.extend({
botToken: z.string(),
chatId: z.string(),
webhookUrl: z.string(),
channel: z.string(),
smtpServer: z.string(),
smtpPort: z.string(),
fromAddress: z.string(),
username: z.string(),
password: z.string(),
toAddresses: z.array(z.string()),
})
.partial();