Merge remote-tracking branch 'upstream/canary' into feat/domain-enable-disable

# Conflicts:
#	apps/dokploy/drizzle/meta/0182_snapshot.json
#	apps/dokploy/drizzle/meta/_journal.json
This commit is contained in:
Vyacheslav Scherbinin
2026-08-11 22:59:41 +07:00
48 changed files with 10320 additions and 521 deletions
+1 -1
View File
@@ -112,7 +112,7 @@
"tailwindcss": "^3.4.17",
"tsc-alias": "1.8.10",
"tsx": "^4.22.4",
"typescript": "^5.8.3"
"typescript": "^7.0.2"
},
"packageManager": "[email protected]",
"engines": {
+1
View File
@@ -66,6 +66,7 @@ export const organization = pgTable("organization", {
logo: text("logo"),
createdAt: timestamp("created_at").notNull(),
metadata: text("metadata"),
defaultRole: text("default_role"),
ownerId: text("owner_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
@@ -0,0 +1,91 @@
import {
Body,
Container,
Head,
Heading,
Html,
Img,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
import { emailTailwindConfig } from "../tailwind-config";
export type TemplateProps = {
serverName: string;
type: "CPU" | "Memory";
value: string;
threshold: string;
message: string;
date: string;
};
export const ServerThresholdEmail = ({
serverName = "my-server",
type = "CPU",
value = "95.00",
threshold = "90.00",
message = "Resource usage exceeded the configured threshold",
date = "2023-05-01T00:00:00.000Z",
}: TemplateProps) => {
const previewText = `Server ${type} alert for ${serverName} ⚠️`;
return (
<Html>
<Preview>{previewText}</Preview>
<Tailwind config={emailTailwindConfig}>
<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://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
}
width="100"
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">
⚠️ Server {type} alert for <strong>{serverName}</strong>
</Heading>
<Text className="text-black text-[14px] leading-[24px]">
Hello,
</Text>
<Text className="text-black text-[14px] leading-[24px]">
The {type} usage on <strong>{serverName}</strong> exceeded the
configured threshold.
</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">
Server Name: <strong>{serverName}</strong>
</Text>
<Text className="!leading-3">
Type: <strong>{type}</strong>
</Text>
<Text className="!leading-3">
Current Value: <strong>{value}%</strong>
</Text>
<Text className="!leading-3">
Threshold: <strong>{threshold}%</strong>
</Text>
<Text className="!leading-3">
Date: <strong>{date}</strong>
</Text>
</Section>
<Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
<Text className="!leading-3 font-bold">Message: </Text>
<Text className="text-[12px] leading-[24px]">{message}</Text>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
};
export default ServerThresholdEmail;
+5 -1
View File
@@ -18,6 +18,7 @@ import {
getUserByToken,
} from "../services/admin";
import { createAuditLog } from "../services/proprietary/audit-log";
import { resolveOrganizationDefaultRole } from "../services/proprietary/license-key";
import {
getWebServerSettings,
updateWebServerSettings,
@@ -289,10 +290,13 @@ const createBetterAuth = () =>
message: "Provider not found",
});
}
const defaultRole = provider.organizationId
? await resolveOrganizationDefaultRole(provider.organizationId)
: "member";
await db.insert(schema.member).values({
userId: user.id,
organizationId: provider?.organizationId || "",
role: "member",
role: defaultRole,
createdAt: new Date(),
isDefault: true,
});
+2 -2
View File
@@ -802,7 +802,7 @@ export const findAllDeploymentsByComposeId = async (composeId: string) => {
const centralizedDeploymentsWith = {
application: {
columns: { applicationId: true, name: true, appName: true },
columns: { applicationId: true, name: true, appName: true, icon: true },
with: {
environment: {
columns: { environmentId: true, name: true },
@@ -821,7 +821,7 @@ const centralizedDeploymentsWith = {
},
},
compose: {
columns: { composeId: true, name: true, appName: true },
columns: { composeId: true, name: true, appName: true, icon: true },
with: {
environment: {
columns: { environmentId: true, name: true },
@@ -1,6 +1,10 @@
import { db } from "@dokploy/server/db";
import { user } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
import {
organization,
organizationRole,
user,
} from "@dokploy/server/db/schema";
import { and, eq } from "drizzle-orm";
import { getOrganizationOwnerId } from "./sso";
export const hasValidLicense = async (organizationId: string) => {
@@ -22,3 +26,35 @@ export const hasValidLicense = async (organizationId: string) => {
currentUser?.isValidEnterpriseLicense
);
};
export const resolveOrganizationDefaultRole = async (
organizationId: string,
) => {
const org = await db.query.organization.findFirst({
where: eq(organization.id, organizationId),
columns: { defaultRole: true },
});
const defaultRole = org?.defaultRole;
if (!defaultRole || defaultRole === "owner") {
return "member";
}
if (defaultRole === "admin" || defaultRole === "member") {
return defaultRole;
}
const customRole = await db.query.organizationRole.findFirst({
where: and(
eq(organizationRole.organizationId, organizationId),
eq(organizationRole.role, defaultRole),
),
columns: { id: true },
});
if (!customRole || !(await hasValidLicense(organizationId))) {
return "member";
}
return defaultRole;
};
+1 -1
View File
@@ -136,7 +136,7 @@ export function processValue(
if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) {
return generateJwt({ length: Number.parseInt(params[0], 10) });
}
let [secret, payload] = params;
let [secret, payload] = params as [string?, string?];
if (typeof payload === "string" && variables[payload]) {
payload = variables[payload];
}
+6 -2
View File
@@ -6,6 +6,7 @@ import { network, patch } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose";
import type { Domain } from "@dokploy/server/services/domain";
import { eq, inArray } from "drizzle-orm";
import { quote } from "shell-quote";
import { parse, stringify } from "yaml";
import { execAsyncRemote } from "../process/execAsync";
import { cloneBitbucketRepository } from "../providers/bitbucket";
@@ -128,8 +129,11 @@ exit 1;
const encodedContent = encodeBase64(composeString);
return `echo "${encodedContent}" | base64 -d > "${path}";`;
} catch (error) {
// @ts-ignore
return `echo "❌ Has occurred an error: ${error?.message || error}";
const message =
error instanceof Error ? error.message : String(error ?? "");
// The error message embeds user-controlled fields (e.g. serviceName) and is
// executed as part of the compose build shell script, so it must be escaped.
return `echo ${quote([`❌ Has occurred an error: ${message}`])};
exit 1;
`;
}
@@ -1,12 +1,18 @@
import { render } from "@react-email/components";
import { and, eq } from "drizzle-orm";
import { db } from "../../db";
import { notifications } from "../../db/schema";
import ServerThresholdEmail from "../../emails/emails/server-threshold";
import {
sendCustomNotification,
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendLarkNotification,
sendMattermostNotification,
sendNtfyNotification,
sendPushoverNotification,
sendResendNotification,
sendSlackNotification,
sendTeamsNotification,
sendTelegramNotification,
@@ -39,6 +45,9 @@ export const sendServerThresholdNotifications = async (
discord: true,
telegram: true,
slack: true,
resend: true,
gotify: true,
ntfy: true,
mattermost: true,
custom: true,
lark: true,
@@ -52,9 +61,13 @@ export const sendServerThresholdNotifications = async (
for (const notification of notificationList) {
const {
email,
discord,
telegram,
slack,
resend,
gotify,
ntfy,
mattermost,
custom,
lark,
@@ -63,6 +76,64 @@ export const sendServerThresholdNotifications = async (
} = notification;
try {
if (email || resend) {
const template = await render(
ServerThresholdEmail({
serverName: payload.ServerName,
type: payload.Type,
value: payload.Value.toFixed(2),
threshold: payload.Threshold.toFixed(2),
message: payload.Message,
date: date.toLocaleString(),
}),
);
if (email) {
await sendEmailNotification(
email,
`Server ${payload.Type} alert for ${payload.ServerName}`,
template,
);
}
if (resend) {
await sendResendNotification(
resend,
`Server ${payload.Type} alert for ${payload.ServerName}`,
template,
);
}
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("⚠️", `Server ${payload.Type} Alert`),
`${decorate("🏷️", `Server: ${payload.ServerName}`)}` +
`${decorate("📊", `Current Value: ${payload.Value.toFixed(2)}%`)}` +
`${decorate("⚠️", `Threshold: ${payload.Threshold.toFixed(2)}%`)}` +
`${decorate("📜", `Message: ${payload.Message}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
`Server ${payload.Type} Alert`,
"warning",
"",
`🏷️Server: ${payload.ServerName}\n` +
`📊Current Value: ${payload.Value.toFixed(2)}%\n` +
`⚠️Threshold: ${payload.Threshold.toFixed(2)}%\n` +
`📜Message: ${payload.Message}\n` +
`🕒Date: ${date.toLocaleString()}`,
);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
+7 -1
View File
@@ -79,6 +79,10 @@ const generateRestoreCommand = (
}
};
// Dumps taken with `--databases` carry `USE`/`CREATE DATABASE` statements that
// would redirect the restore away from the database selected in the dialog.
export const stripDatabaseSwitchCommand = `grep -viE '^[[:space:]]*(use|create[[:space:]]+database)[[:space:]]'`;
const getMongoSpecificCommand = (
rcloneCommand: string,
restoreCommand: string,
@@ -125,7 +129,9 @@ export const getRestoreCommand = ({
const restoreCommand = generateRestoreCommand(type, credentials);
let cmd = `CONTAINER_ID=$(${containerSearch})`;
if (type !== "mongo") {
if (type === "mysql" || type === "mariadb") {
cmd += ` && ${rcloneCommand} | ${stripDatabaseSwitchCommand} | ${restoreCommand}`;
} else if (type !== "mongo") {
cmd += ` && ${rcloneCommand} | ${restoreCommand}`;
} else {
cmd += ` && ${getMongoSpecificCommand(rcloneCommand, restoreCommand, backupFile || "")}`;
-1
View File
@@ -21,7 +21,6 @@
"plugins": [{ "name": "next" }],
"outDir": "./dist",
/* Path Aliases */
"baseUrl": ".",
"paths": {
"@dokploy/server/*": ["./src/*"]
}
+1 -2
View File
@@ -8,9 +8,8 @@
"isolatedModules": false,
"noEmit": false,
"declaration": true,
"moduleResolution": "Node",
"moduleResolution": "bundler",
"rootDir": "./src",
"baseUrl": ".",
"jsx": "react-jsx",
"paths": {
"@dokploy/server/*": ["./src/*"]