Merge branch 'canary' into feature/custom-entrypoint

# Conflicts:
#	apps/dokploy/drizzle/meta/0117_snapshot.json
#	apps/dokploy/drizzle/meta/_journal.json
This commit is contained in:
mkarpats
2025-10-26 14:26:52 +02:00
59 changed files with 15521 additions and 311 deletions

View File

@@ -80,6 +80,7 @@ export const applications = pgTable("application", {
previewEnv: text("previewEnv"),
watchPaths: text("watchPaths").array(),
previewBuildArgs: text("previewBuildArgs"),
previewBuildSecrets: text("previewBuildSecrets"),
previewLabels: text("previewLabels").array(),
previewWildcard: text("previewWildcard"),
previewPort: integer("previewPort").default(3000),
@@ -99,6 +100,7 @@ export const applications = pgTable("application", {
).default(true),
rollbackActive: boolean("rollbackActive").default(false),
buildArgs: text("buildArgs"),
buildSecrets: text("buildSecrets"),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
@@ -253,6 +255,7 @@ const createSchema = createInsertSchema(applications, {
autoDeploy: z.boolean(),
env: z.string().optional(),
buildArgs: z.string().optional(),
buildSecrets: z.string().optional(),
name: z.string().min(1),
description: z.string().optional(),
memoryReservation: z.string().optional(),
@@ -304,6 +307,7 @@ const createSchema = createInsertSchema(applications, {
previewPort: z.number().optional(),
previewEnv: z.string().optional(),
previewBuildArgs: z.string().optional(),
previewBuildSecrets: z.string().optional(),
previewWildcard: z.string().optional(),
previewLimit: z.number().optional(),
previewHttps: z.boolean().optional(),
@@ -458,6 +462,7 @@ export const apiSaveEnvironmentVariables = createSchema
applicationId: true,
env: true,
buildArgs: true,
buildSecrets: true,
})
.required();

View File

@@ -12,7 +12,6 @@ import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
import { mounts } from "./mount";
import { projects } from "./project";
import { schedules } from "./schedule";
import { server } from "./server";
import { applicationStatus, triggerType } from "./shared";

View File

@@ -12,6 +12,7 @@ export const notificationType = pgEnum("notificationType", [
"email",
"gotify",
"ntfy",
"lark",
]);
export const notifications = pgTable("notification", {
@@ -48,6 +49,9 @@ export const notifications = pgTable("notification", {
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
onDelete: "cascade",
}),
larkId: text("larkId").references(() => lark.larkId, {
onDelete: "cascade",
}),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
@@ -116,6 +120,14 @@ export const ntfy = pgTable("ntfy", {
priority: integer("priority").notNull().default(3),
});
export const lark = pgTable("lark", {
larkId: text("larkId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
webhookUrl: text("webhookUrl").notNull(),
});
export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, {
fields: [notifications.slackId],
@@ -141,6 +153,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.ntfyId],
references: [ntfy.ntfyId],
}),
lark: one(lark, {
fields: [notifications.larkId],
references: [lark.larkId],
}),
organization: one(organization, {
fields: [notifications.organizationId],
references: [organization.id],
@@ -339,6 +355,31 @@ export const apiFindOneNotification = notificationsSchema
})
.required();
export const apiCreateLark = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
webhookUrl: z.string().min(1),
})
.required();
export const apiUpdateLark = apiCreateLark.partial().extend({
notificationId: z.string().min(1),
larkId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestLarkConnection = apiCreateLark.pick({
webhookUrl: true,
});
export const apiSendTest = notificationsSchema
.extend({
botToken: z.string(),

View File

@@ -68,7 +68,6 @@ export * from "./utils/backups/postgres";
export * from "./utils/backups/utils";
export * from "./utils/backups/web-server";
export * from "./utils/builders/compose";
export * from "./utils/startup/cancell-deployments";
export * from "./utils/builders/docker-file";
export * from "./utils/builders/drop";
export * from "./utils/builders/heroku";
@@ -77,7 +76,6 @@ export * from "./utils/builders/nixpacks";
export * from "./utils/builders/paketo";
export * from "./utils/builders/static";
export * from "./utils/builders/utils";
export * from "./utils/cluster/upload";
export * from "./utils/databases/rebuild";
export * from "./utils/docker/collision";
@@ -113,6 +111,8 @@ export * from "./utils/providers/raw";
export * from "./utils/schedules/index";
export * from "./utils/schedules/utils";
export * from "./utils/servers/remote-docker";
export * from "./utils/startup/cancell-deployments";
export * from "./utils/tracking/hubspot";
export * from "./utils/traefik/application";
export * from "./utils/traefik/domain";
export * from "./utils/traefik/file-types";

View File

@@ -10,6 +10,7 @@ import { db } from "../db";
import * as schema from "../db/schema";
import { getUserByToken } from "../services/admin";
import { updateUser } from "../services/user";
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
import { sendEmail } from "../verification/send-verification-email";
import { getPublicIpWithFallback } from "../wss/utils";
@@ -115,7 +116,7 @@ const { handler, api } = betterAuth({
}
}
},
after: async (user) => {
after: async (user, context) => {
const isAdminPresent = await db.query.member.findFirst({
where: eq(schema.member.role, "owner"),
});
@@ -126,6 +127,27 @@ const { handler, api } = betterAuth({
});
}
if (IS_CLOUD) {
try {
const hutk = getHubSpotUTK(
context?.request?.headers?.get("cookie") || undefined,
);
const hubspotSuccess = await submitToHubSpot(
{
email: user.email,
firstName: user.name,
lastName: user.name,
},
hutk,
);
if (!hubspotSuccess) {
console.error("Failed to submit to HubSpot");
}
} catch (error) {
console.error("Error submitting to HubSpot", error);
}
}
if (IS_CLOUD || !isAdminPresent) {
await db.transaction(async (tx) => {
const organization = await tx

View File

@@ -472,7 +472,8 @@ export const deployPreviewApplication = async ({
});
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = application.previewBuildArgs;
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
if (application.sourceType === "github") {
await cloneGithubRepository({
@@ -579,7 +580,8 @@ export const deployRemotePreviewApplication = async ({
});
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = application.previewBuildArgs;
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
if (application.serverId) {
let command = "set -e;";
@@ -666,7 +668,6 @@ export const rebuildRemoteApplication = async ({
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
);
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
throw error;

View File

@@ -2,18 +2,21 @@ import { db } from "@dokploy/server/db";
import {
type apiCreateDiscord,
type apiCreateEmail,
type apiCreateLark,
type apiCreateGotify,
type apiCreateNtfy,
type apiCreateSlack,
type apiCreateTelegram,
type apiUpdateDiscord,
type apiUpdateEmail,
type apiUpdateLark,
type apiUpdateGotify,
type apiUpdateNtfy,
type apiUpdateSlack,
type apiUpdateTelegram,
discord,
email,
lark,
gotify,
notifications,
ntfy,
@@ -585,6 +588,7 @@ export const findNotificationById = async (notificationId: string) => {
email: true,
gotify: true,
ntfy: true,
lark: true,
},
});
if (!notification) {
@@ -605,6 +609,94 @@ export const removeNotificationById = async (notificationId: string) => {
return result[0];
};
export const createLarkNotification = async (
input: typeof apiCreateLark._type,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newLark = await tx
.insert(lark)
.values({
webhookUrl: input.webhookUrl,
})
.returning()
.then((value) => value[0]);
if (!newLark) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting lark",
});
}
const newDestination = await tx
.insert(notifications)
.values({
larkId: newLark.larkId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "lark",
organizationId: organizationId,
serverThreshold: input.serverThreshold,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updateLarkNotification = async (
input: typeof apiUpdateLark._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(lark)
.set({
webhookUrl: input.webhookUrl,
})
.where(eq(lark.larkId, input.larkId))
.returning()
.then((value) => value[0]);
return newDestination;
});
};
export const updateNotificationById = async (
notificationId: string,
notificationData: Partial<Notification>,

View File

@@ -141,8 +141,8 @@ export function processValue(
}
if (
typeof payload === "string" &&
payload.startsWith("{") &&
payload.endsWith("}")
payload.trimStart().startsWith("{") &&
payload.trimEnd().endsWith("}")
) {
try {
payload = JSON.parse(payload);

View File

@@ -16,6 +16,7 @@ export function getProviderName(apiUrl: string) {
if (apiUrl.includes("api.mistral.ai")) return "mistral";
if (apiUrl.includes(":11434") || apiUrl.includes("ollama")) return "ollama";
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
if (apiUrl.includes("generativelanguage.googleapis.com")) return "gemini";
return "custom";
}
@@ -66,6 +67,13 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
baseURL: config.apiUrl,
apiKey: config.apiKey,
});
case "gemini":
return createOpenAICompatible({
name: "gemini",
baseURL: config.apiUrl,
queryParams: { key: config.apiKey },
headers: {},
});
case "custom":
return createOpenAICompatible({
name: "custom",

View File

@@ -1,5 +1,8 @@
import type { WriteStream } from "node:fs";
import { prepareEnvironmentVariables } from "@dokploy/server/utils/docker/utils";
import {
getEnviromentVariablesObject,
prepareEnvironmentVariables,
} from "@dokploy/server/utils/docker/utils";
import {
getBuildAppDirectory,
getDockerContextPath,
@@ -17,6 +20,7 @@ export const buildCustomDocker = async (
env,
publishDirectory,
buildArgs,
buildSecrets,
dockerBuildStage,
cleanCache,
} = application;
@@ -26,11 +30,6 @@ export const buildCustomDocker = async (
const defaultContextPath =
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
const dockerContextPath = getDockerContextPath(application);
@@ -44,9 +43,29 @@ export const buildCustomDocker = async (
commandArgs.push("--target", dockerBuildStage);
}
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
for (const arg of args) {
commandArgs.push("--build-arg", arg);
}
const secrets = getEnviromentVariablesObject(
buildSecrets,
application.environment.project.env,
application.environment.env,
);
for (const key in secrets) {
// Although buildx is smart enough to know we may be referring to an environment variable name,
// we still make sure it doesn't fall back to type=file.
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
commandArgs.push("--secret", `type=env,id=${key}`);
}
/*
Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed.
@@ -70,6 +89,10 @@ export const buildCustomDocker = async (
},
{
cwd: dockerContextPath || defaultContextPath,
env: {
...process.env,
...secrets,
},
},
);
} catch (error) {
@@ -86,6 +109,7 @@ export const getDockerCommand = (
env,
publishDirectory,
buildArgs,
buildSecrets,
dockerBuildStage,
cleanCache,
} = application;
@@ -96,11 +120,6 @@ export const getDockerCommand = (
const defaultContextPath =
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
const dockerContextPath =
getDockerContextPath(application) || defaultContextPath;
@@ -115,10 +134,33 @@ export const getDockerCommand = (
commandArgs.push("--no-cache");
}
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
for (const arg of args) {
commandArgs.push("--build-arg", `'${arg}'`);
}
const secrets = getEnviromentVariablesObject(
buildSecrets,
application.environment.project.env,
application.environment.env,
);
const joinedSecrets = Object.entries(secrets)
.map(([key, value]) => `${key}='${value.replace(/'/g, "'\"'\"'")}'`)
.join(" ");
for (const key in secrets) {
// Although buildx is smart enough to know we may be referring to an environment variable name,
// we still make sure it doesn't fall back to `type=file`.
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
commandArgs.push("--secret", `type=env,id=${key}`);
}
/*
Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed.
@@ -140,7 +182,7 @@ cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {
exit 1;
}
docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
${joinedSecrets} docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
echo "❌ Docker build failed" >> ${logPath};
exit 1;
}

View File

@@ -7,6 +7,7 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
@@ -44,11 +45,13 @@ export const sendBuildErrorNotifications = async ({
slack: true,
gotify: true,
ntfy: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
BuildFailedEmail({
@@ -211,5 +214,117 @@ export const sendBuildErrorNotifications = async ({
],
});
}
if (lark) {
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content: "⚠️ Build Failed",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "red",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Type:**\n${applicationType}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
},
type: "danger",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
},
],
margin: "0px 0px 0px 0px",
},
],
},
},
});
}
}
};

View File

@@ -8,6 +8,7 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
@@ -45,11 +46,13 @@ export const sendBuildSuccessNotifications = async ({
slack: true,
gotify: true,
ntfy: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
@@ -210,5 +213,109 @@ export const sendBuildSuccessNotifications = async ({
],
});
}
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content: "✅ Build Success",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "green",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Type:**\n${applicationType}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
},
type: "primary",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
},
],
margin: "0px 0px 0px 0px",
},
],
},
},
});
}
}
};

View File

@@ -7,6 +7,7 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
@@ -44,11 +45,13 @@ export const sendDatabaseBackupNotifications = async ({
slack: true,
gotify: true,
ntfy: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
@@ -238,5 +241,120 @@ export const sendDatabaseBackupNotifications = async ({
],
});
}
if (lark) {
const limitCharacter = 800;
const truncatedErrorMessage =
errorMessage && errorMessage.length > limitCharacter
? errorMessage.substring(0, limitCharacter)
: errorMessage;
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content:
type === "success"
? "✅ Database Backup Successful"
: "❌ Database Backup Failed",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: type === "success" ? "green" : "red",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Database Type:**\n${databaseType}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Database Name:**\n${databaseName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
...(type === "error" && truncatedErrorMessage
? [
{
tag: "markdown",
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
text_align: "left",
text_size: "normal_v2",
},
]
: []),
],
},
},
});
}
}
};

View File

@@ -7,6 +7,7 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
@@ -31,11 +32,13 @@ export const sendDockerCleanupNotifications = async (
slack: true,
gotify: true,
ntfy: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
@@ -135,5 +138,83 @@ export const sendDockerCleanupNotifications = async (
],
});
}
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content: "✅ Docker Cleanup",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "green",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Status:**\nSuccessful`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Cleanup Details:**\n${message}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
],
},
},
});
}
}
};

View File

@@ -7,6 +7,7 @@ import { eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
@@ -25,11 +26,13 @@ export const sendDokployRestartNotifications = async () => {
slack: true,
gotify: true,
ntfy: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
@@ -135,5 +138,81 @@ export const sendDokployRestartNotifications = async () => {
console.log(error);
}
}
if (lark) {
try {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content: "✅ Dokploy Server Restarted",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "green",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Status:**\nSuccessful`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Restart Time:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
],
},
},
});
} catch (error) {
console.log(error);
}
}
}
};

View File

@@ -3,6 +3,7 @@ import { db } from "../../db";
import { notifications } from "../../db/schema";
import {
sendDiscordNotification,
sendLarkNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -34,6 +35,7 @@ export const sendServerThresholdNotifications = async (
discord: true,
telegram: true,
slack: true,
lark: true,
},
});
@@ -41,7 +43,7 @@ export const sendServerThresholdNotifications = async (
const typeColor = 0xff0000; // Rojo para indicar alerta
for (const notification of notificationList) {
const { discord, telegram, slack } = notification;
const { discord, telegram, slack, lark } = notification;
if (discord) {
const decorate = (decoration: string, text: string) =>
@@ -151,5 +153,101 @@ export const sendServerThresholdNotifications = async (
],
});
}
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content: `⚠️ Server ${payload.Type} Alert`,
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "red",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Server Name:**\n${payload.ServerName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Current Value:**\n${payload.Value.toFixed(2)}%`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Alert Message:**\n${payload.Message}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Type:**\n${payload.Type === "CPU" ? "🔲" : "💾"} ${payload.Type}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Threshold:**\n${payload.Threshold.toFixed(2)}%`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Alert Time:**\n${date.toLocaleString()}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
],
},
},
});
}
}
};

View File

@@ -1,6 +1,7 @@
import type {
discord,
email,
lark,
gotify,
ntfy,
slack,
@@ -152,3 +153,18 @@ export const sendNtfyNotification = async (
throw new Error(`Failed to send ntfy notification: ${response.statusText}`);
}
};
export const sendLarkNotification = async (
connection: typeof lark.$inferInsert,
message: any,
) => {
try {
await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message),
});
} catch (err) {
console.log(err);
}
};

View File

@@ -0,0 +1,125 @@
interface HubSpotFormField {
objectTypeId: string;
name: string;
value: string;
}
interface HubSpotFormData {
fields: HubSpotFormField[];
context: {
pageUri: string;
pageName: string;
hutk?: string; // HubSpot UTK from cookies
};
}
interface SignUpFormData {
firstName?: string;
lastName?: string;
email?: string;
}
/**
* Extract HubSpot UTK (User Token) from cookies
* This is used for tracking and attribution in HubSpot
*/
export function getHubSpotUTK(cookieHeader?: string): string | null {
if (!cookieHeader) return null;
const name = "hubspotutk=";
const decodedCookie = decodeURIComponent(cookieHeader);
const cookieArray = decodedCookie.split(";");
for (let i = 0; i < cookieArray.length; i++) {
const cookie = cookieArray[i]?.trim();
if (!cookie) continue;
if (cookie.indexOf(name) === 0) {
return cookie.substring(name.length, cookie.length);
}
}
return null;
}
/**
* Convert contact form data to HubSpot form format
*/
export function formatContactDataForHubSpot(
contactData: SignUpFormData,
hutk?: string | null,
): HubSpotFormData {
const formData: HubSpotFormData = {
fields: [
{
objectTypeId: "0-1", // Contact object type
name: "firstname",
value: contactData.firstName || "",
},
{
objectTypeId: "0-1",
name: "lastname",
value: contactData.lastName || "",
},
{
objectTypeId: "0-1",
name: "email",
value: contactData.email || "",
},
],
context: {
pageUri: "https://app.dokploy.com/register",
pageName: "Sign Up",
},
};
// Add HubSpot UTK if available
if (hutk) {
formData.context.hutk = hutk;
}
return formData;
}
/**
* Submit form data to HubSpot Forms API
*/
export async function submitToHubSpot(
contactData: SignUpFormData,
hutk?: string | null,
): Promise<boolean> {
try {
const portalId = process.env.HUBSPOT_PORTAL_ID;
const formGuid = process.env.HUBSPOT_FORM_GUID;
if (!portalId || !formGuid) {
console.error(
"HubSpot configuration missing: HUBSPOT_PORTAL_ID or HUBSPOT_FORM_GUID not set",
);
return false;
}
const formData = formatContactDataForHubSpot(contactData, hutk);
const response = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${portalId}/${formGuid}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
},
);
if (!response.ok) {
const errorText = await response.text();
console.error("HubSpot API error:", response.status, errorText);
return false;
}
const result = await response.json();
console.log("HubSpot submission successful:", result);
return true;
} catch (error) {
console.error("Error submitting to HubSpot:", error);
return false;
}
}