Merge branch 'canary' into bitbucket-api-token

This commit is contained in:
Mauricio Siu
2025-09-21 03:10:37 -06:00
98 changed files with 8239 additions and 471 deletions

View File

@@ -92,31 +92,48 @@ export const suggestVariants = async ({
const { object } = await generateObject({
model,
output: "array",
output: "object",
schema: z.object({
id: z.string(),
name: z.string(),
shortDescription: z.string(),
description: z.string(),
suggestions: z.array(
z.object({
id: z.string(),
name: z.string(),
shortDescription: z.string(),
description: z.string(),
}),
),
}),
prompt: `
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items), the suggestion
should include id, name, shortDescription, and description. Use slug of title for id.
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items).
Return your response as a JSON object with the following structure:
{
"suggestions": [
{
"id": "project-slug",
"name": "Project Name",
"shortDescription": "Brief one-line description",
"description": "Detailed description"
}
]
}
Important rules for the response:
1. The description field should ONLY contain a plain text description of the project, its features, and use cases
2. Do NOT include any code snippets, configuration examples, or installation instructions in the description
3. The shortDescription should be a single-line summary focusing on the main technologies
1. Use slug format for the id field (lowercase, hyphenated)
2. The description field should ONLY contain a plain text description of the project, its features, and use cases
3. Do NOT include any code snippets, configuration examples, or installation instructions in the description
4. The shortDescription should be a single-line summary focusing on the main technologies
5. All projects should be installable in docker and have docker compose support
User wants to create a new project with the following details, it should be installable in docker and can be docker compose generated for it:
User wants to create a new project with the following details:
${input}
`,
});
if (object?.length) {
if (object?.suggestions?.length) {
const result = [];
for (const suggestion of object) {
for (const suggestion of object.suggestions) {
try {
const { object: docker } = await generateObject({
model,
@@ -136,16 +153,29 @@ export const suggestVariants = async ({
serviceName: z.string(),
}),
),
configFiles: z.array(
z.object({
content: z.string(),
filePath: z.string(),
}),
),
configFiles: z
.array(
z.object({
content: z.string(),
filePath: z.string(),
}),
)
.optional(),
}),
prompt: `
Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project.
Return the docker compose as a YAML string and environment variables configuration. Follow these rules:
Return your response as a JSON object with this structure:
{
"dockerCompose": "yaml string here",
"envVariables": [{"name": "VAR_NAME", "value": "example_value"}],
"domains": [{"host": "domain.com", "port": 3000, "serviceName": "service"}],
"configFiles": [{"content": "file content", "filePath": "path/to/file"}]
}
Note: configFiles is optional - only include it if configuration files are absolutely required.
Follow these rules:
Docker Compose Rules:
1. Use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker-compose.yml
@@ -198,6 +228,7 @@ export const suggestVariants = async ({
console.error("Error in docker compose generation:", error);
}
}
return result;
}

View File

@@ -9,7 +9,7 @@ import {
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import { stringify } from "yaml";
import type { z } from "zod";
import { encodeBase64 } from "../utils/docker/utils";
import { execAsyncRemote } from "../utils/process/execAsync";
@@ -101,7 +101,7 @@ const createCertificateFiles = async (certificate: Certificate) => {
],
},
};
const yamlConfig = dump(traefikConfig);
const yamlConfig = stringify(traefikConfig);
const configFile = path.join(certDir, "certificate.yml");
if (certificate.serverId) {

View File

@@ -3,17 +3,20 @@ import {
type apiCreateDiscord,
type apiCreateEmail,
type apiCreateGotify,
type apiCreateNtfy,
type apiCreateSlack,
type apiCreateTelegram,
type apiUpdateDiscord,
type apiUpdateEmail,
type apiUpdateGotify,
type apiUpdateNtfy,
type apiUpdateSlack,
type apiUpdateTelegram,
discord,
email,
gotify,
notifications,
ntfy,
slack,
telegram,
} from "@dokploy/server/db/schema";
@@ -482,6 +485,96 @@ export const updateGotifyNotification = async (
});
};
export const createNtfyNotification = async (
input: typeof apiCreateNtfy._type,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newNtfy = await tx
.insert(ntfy)
.values({
serverUrl: input.serverUrl,
topic: input.topic,
accessToken: input.accessToken,
priority: input.priority,
})
.returning()
.then((value) => value[0]);
if (!newNtfy) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting ntfy",
});
}
const newDestination = await tx
.insert(notifications)
.values({
ntfyId: newNtfy.ntfyId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "ntfy",
organizationId: organizationId,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updateNtfyNotification = async (
input: typeof apiUpdateNtfy._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,
})
.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(ntfy)
.set({
serverUrl: input.serverUrl,
topic: input.topic,
accessToken: input.accessToken,
priority: input.priority,
})
.where(eq(ntfy.ntfyId, input.ntfyId));
return newDestination;
});
};
export const findNotificationById = async (notificationId: string) => {
const notification = await db.query.notifications.findFirst({
where: eq(notifications.notificationId, notificationId),
@@ -491,6 +584,7 @@ export const findNotificationById = async (notificationId: string) => {
discord: true,
email: true,
gotify: true,
ntfy: true,
},
});
if (!notification) {

View File

@@ -10,6 +10,22 @@ import { IS_CLOUD } from "../constants";
export type Registry = typeof registry.$inferSelect;
function shEscape(s: string | undefined): string {
if (!s) return "''";
return `'${s.replace(/'/g, `'\\''`)}'`;
}
function safeDockerLoginCommand(
registry: string | undefined,
user: string | undefined,
pass: string | undefined,
) {
const escapedRegistry = shEscape(registry);
const escapedUser = shEscape(user);
const escapedPassword = shEscape(pass);
return `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`;
}
export const createRegistry = async (
input: typeof apiCreateRegistry._type,
organizationId: string,
@@ -37,7 +53,11 @@ export const createRegistry = async (
message: "Select a server to add the registry",
});
}
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
const loginCommand = safeDockerLoginCommand(
input.registryUrl,
input.username,
input.password,
);
if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand);
} else if (newRegistry.registryType === "cloud") {
@@ -91,7 +111,11 @@ export const updateRegistry = async (
.returning()
.then((res) => res[0]);
const loginCommand = `echo ${response?.password} | docker login ${response?.registryUrl} --username ${response?.username} --password-stdin`;
const loginCommand = safeDockerLoginCommand(
response?.registryUrl,
response?.username,
response?.password,
);
if (
IS_CLOUD &&

View File

@@ -342,6 +342,8 @@ export const readPorts = async (
command = `docker service inspect ${resourceName} --format '{{json .Spec.EndpointSpec.Ports}}'`;
} else if (resourceType === "standalone") {
command = `docker container inspect ${resourceName} --format '{{json .NetworkSettings.Ports}}'`;
} else {
throw new Error("Resource type not found");
}
let result = "";
if (serverId) {
@@ -397,17 +399,20 @@ export const writeTraefikSetup = async (input: TraefikOptions) => {
"dokploy-traefik",
input.serverId,
);
if (resourceType === "service") {
await initializeTraefikService({
env: input.env,
additionalPorts: input.additionalPorts,
serverId: input.serverId,
});
} else {
} else if (resourceType === "standalone") {
await initializeStandaloneTraefik({
env: input.env,
additionalPorts: input.additionalPorts,
serverId: input.serverId,
});
} else {
throw new Error("Traefik resource type not found");
}
};

View File

@@ -296,6 +296,19 @@ export const findMemberById = async (
};
export const updateUser = async (userId: string, userData: Partial<User>) => {
// Validate email if it's being updated
if (userData.email !== undefined) {
if (!userData.email || userData.email.trim() === "") {
throw new Error("Email is required and cannot be empty");
}
// Basic email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userData.email)) {
throw new Error("Please enter a valid email address");
}
}
const user = await db
.update(users_temp)
.set({