mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(notifications): WIP add schema and modal
This commit is contained in:
@@ -23,6 +23,7 @@ import { dockerRouter } from "./routers/docker";
|
||||
import { composeRouter } from "./routers/compose";
|
||||
import { registryRouter } from "./routers/registry";
|
||||
import { clusterRouter } from "./routers/cluster";
|
||||
import { notificationRouter } from "./routers/notification";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@@ -54,6 +55,7 @@ export const appRouter = createTRPCRouter({
|
||||
port: portRouter,
|
||||
registry: registryRouter,
|
||||
cluster: clusterRouter,
|
||||
notification: notificationRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
150
server/api/routers/notification.ts
Normal file
150
server/api/routers/notification.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
} from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateDestination,
|
||||
apiCreateDiscord,
|
||||
apiCreateEmail,
|
||||
apiCreateSlack,
|
||||
apiCreateTelegram,
|
||||
apiFindOneNotification,
|
||||
apiUpdateDestination,
|
||||
} from "@/server/db/schema";
|
||||
import { HeadBucketCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { updateDestinationById } from "../services/destination";
|
||||
import {
|
||||
createDiscordNotification,
|
||||
createEmailNotification,
|
||||
createSlackNotification,
|
||||
createTelegramNotification,
|
||||
findNotificationById,
|
||||
removeNotificationById,
|
||||
} from "../services/notification";
|
||||
|
||||
export const notificationRouter = createTRPCRouter({
|
||||
createSlack: adminProcedure
|
||||
.input(apiCreateSlack)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await createSlackNotification(input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the destination",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
createTelegram: adminProcedure
|
||||
.input(apiCreateTelegram)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await createTelegramNotification(input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the destination",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
createDiscord: adminProcedure
|
||||
.input(apiCreateDiscord)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await createDiscordNotification(input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the destination",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
createEmail: adminProcedure
|
||||
.input(apiCreateEmail)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await createEmailNotification(input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the destination",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
removeNotification: adminProcedure
|
||||
.input(apiFindOneNotification)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await removeNotificationById(input.notificationId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this notification",
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneNotification)
|
||||
.query(async ({ input }) => {
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
return notification;
|
||||
}),
|
||||
testConnection: adminProcedure
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
|
||||
const s3Client = new S3Client({
|
||||
region: region,
|
||||
...(endpoint && {
|
||||
endpoint: endpoint,
|
||||
}),
|
||||
credentials: {
|
||||
accessKeyId: accessKey,
|
||||
secretAccessKey: secretAccessKey,
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
const headBucketCommand = new HeadBucketCommand({ Bucket: bucket });
|
||||
|
||||
try {
|
||||
await s3Client.send(headBucketCommand);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to connect to bucket",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
all: adminProcedure.query(async () => {
|
||||
return await db.query.notifications.findMany({
|
||||
with: {
|
||||
slack: true,
|
||||
telegram: true,
|
||||
discord: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
update: adminProcedure
|
||||
.input(apiUpdateDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await updateDestinationById(input.destinationId, input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this destination",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
241
server/api/services/notification.ts
Normal file
241
server/api/services/notification.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
type apiCreateDiscord,
|
||||
type apiCreateEmail,
|
||||
type apiCreateSlack,
|
||||
type apiCreateTelegram,
|
||||
discord,
|
||||
email,
|
||||
notifications,
|
||||
slack,
|
||||
telegram,
|
||||
} from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export type Notification = typeof notifications.$inferSelect;
|
||||
|
||||
export const createSlackNotification = async (
|
||||
input: typeof apiCreateSlack._type,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newSlack = await tx
|
||||
.insert(slack)
|
||||
.values({
|
||||
channel: input.channel,
|
||||
webhookUrl: input.webhookUrl,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newSlack) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting slack",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
slackId: newSlack.slackId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const createTelegramNotification = async (
|
||||
input: typeof apiCreateTelegram._type,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newTelegram = await tx
|
||||
.insert(telegram)
|
||||
.values({
|
||||
botToken: input.botToken,
|
||||
chatId: input.chatId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newTelegram) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting telegram",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
telegramId: newTelegram.telegramId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const createDiscordNotification = async (
|
||||
input: typeof apiCreateDiscord._type,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDiscord = await tx
|
||||
.insert(discord)
|
||||
.values({
|
||||
webhookUrl: input.webhookUrl,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDiscord) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting discord",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
discordId: newDiscord.discordId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const createEmailNotification = async (
|
||||
input: typeof apiCreateEmail._type,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newEmail = await tx
|
||||
.insert(email)
|
||||
.values({
|
||||
smtpServer: input.smtpServer,
|
||||
smtpPort: input.smtpPort,
|
||||
username: input.username,
|
||||
password: input.password,
|
||||
toAddresses: input.toAddresses,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newEmail) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting email",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
emailId: newEmail.emailId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
userJoin: input.userJoin,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const findNotificationById = async (notificationId: string) => {
|
||||
const notification = await db.query.notifications.findFirst({
|
||||
where: eq(notifications.notificationId, notificationId),
|
||||
with: {
|
||||
slack: true,
|
||||
telegram: true,
|
||||
discord: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
if (!notification) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Notification not found",
|
||||
});
|
||||
}
|
||||
return notification;
|
||||
};
|
||||
|
||||
export const removeNotificationById = async (notificationId: string) => {
|
||||
const result = await db
|
||||
.delete(notifications)
|
||||
.where(eq(notifications.notificationId, notificationId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const updateDestinationById = async (
|
||||
notificationId: string,
|
||||
notificationData: Partial<Notification>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(notifications)
|
||||
.set({
|
||||
...notificationData,
|
||||
})
|
||||
.where(eq(notifications.notificationId, notificationId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
@@ -21,3 +21,4 @@ export * from "./redis";
|
||||
export * from "./shared";
|
||||
export * from "./compose";
|
||||
export * from "./registry";
|
||||
export * from "./notification";
|
||||
|
||||
160
server/db/schema/notification.ts
Normal file
160
server/db/schema/notification.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
|
||||
export const notifications = pgTable("notification", {
|
||||
notificationId: text("notificationId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appDeploy: boolean("appDeploy").notNull().default(false),
|
||||
userJoin: boolean("userJoin").notNull().default(false),
|
||||
appBuildError: boolean("appBuildError").notNull().default(false),
|
||||
databaseBackup: boolean("databaseBackup").notNull().default(false),
|
||||
dokployRestart: boolean("dokployRestart").notNull().default(false),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
slackId: text("slackId").references(() => slack.slackId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
telegramId: text("telegramId").references(() => telegram.telegramId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
discordId: text("discordId").references(() => discord.discordId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
emailId: text("emailId").references(() => email.emailId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slack = pgTable("slack", {
|
||||
slackId: text("slackId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
channel: text("channel").notNull(),
|
||||
});
|
||||
|
||||
export const telegram = pgTable("telegram", {
|
||||
telegramId: text("telegramId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
botToken: text("botToken").notNull(),
|
||||
chatId: text("chatId").notNull(),
|
||||
});
|
||||
|
||||
export const discord = pgTable("discord", {
|
||||
discordId: text("discordId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
});
|
||||
|
||||
export const email = pgTable("email", {
|
||||
emailId: text("emailId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
smtpServer: text("smtpServer").notNull(),
|
||||
smtpPort: text("smtpPort").notNull(),
|
||||
username: text("username").notNull(),
|
||||
password: text("password").notNull(),
|
||||
toAddresses: text("toAddress").array().notNull(),
|
||||
});
|
||||
|
||||
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
slack: one(slack, {
|
||||
fields: [notifications.slackId],
|
||||
references: [slack.slackId],
|
||||
}),
|
||||
telegram: one(telegram, {
|
||||
fields: [notifications.telegramId],
|
||||
references: [telegram.telegramId],
|
||||
}),
|
||||
discord: one(discord, {
|
||||
fields: [notifications.discordId],
|
||||
references: [discord.discordId],
|
||||
}),
|
||||
email: one(email, {
|
||||
fields: [notifications.emailId],
|
||||
references: [email.emailId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const notificationsSchema = createInsertSchema(notifications);
|
||||
|
||||
export const apiCreateSlack = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
userJoin: true,
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().min(1),
|
||||
channel: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiCreateTelegram = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
userJoin: true,
|
||||
})
|
||||
.extend({
|
||||
botToken: z.string().min(1),
|
||||
chatId: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiCreateDiscord = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
userJoin: true,
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiCreateEmail = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
userJoin: true,
|
||||
})
|
||||
.extend({
|
||||
smtpServer: z.string().min(1),
|
||||
smtpPort: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
toAddresses: z.array(z.string()).min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneNotification = notificationsSchema
|
||||
.pick({
|
||||
notificationId: true,
|
||||
})
|
||||
.required();
|
||||
Reference in New Issue
Block a user