mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-03 21:15:23 +02:00
Merge branch 'canary' into bitbucket-api-token
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user