From 1635bab44fe1a1ba29a7bae3d9583345bd7c0723 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 24 Aug 2025 23:49:48 -0600 Subject: [PATCH 01/65] =?UTF-8?q?Reapply=20"refactor:=20update=20database?= =?UTF-8?q?=20connection=20handling=20and=20remove=20unused=20migra?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 17f333ac2a5c789f6426c1d56daa44763ee5c7c2. --- apps/dokploy/esbuild.config.ts | 5 + apps/dokploy/migrate.ts | 149 ----------------------- apps/dokploy/server/db/index.ts | 8 +- apps/dokploy/server/db/seed.ts | 29 ----- packages/server/package.json | 2 +- packages/server/src/db/drizzle.config.ts | 14 --- packages/server/src/db/migration.ts | 21 ---- packages/server/src/db/reset.ts | 23 ---- packages/server/src/db/seed.ts | 35 ------ 9 files changed, 12 insertions(+), 274 deletions(-) delete mode 100644 apps/dokploy/migrate.ts delete mode 100644 apps/dokploy/server/db/seed.ts delete mode 100644 packages/server/src/db/drizzle.config.ts delete mode 100644 packages/server/src/db/migration.ts delete mode 100644 packages/server/src/db/reset.ts delete mode 100644 packages/server/src/db/seed.ts diff --git a/apps/dokploy/esbuild.config.ts b/apps/dokploy/esbuild.config.ts index c84135e5d..a1747ac55 100644 --- a/apps/dokploy/esbuild.config.ts +++ b/apps/dokploy/esbuild.config.ts @@ -7,6 +7,10 @@ function prepareDefine(config: DotenvParseOutput | undefined) { const define = {}; // @ts-ignore for (const [key, value] of Object.entries(config)) { + // Skip DATABASE_URL to allow runtime environment variable override + if (key === "DATABASE_URL") { + continue; + } // @ts-ignore define[`process.env.${key}`] = JSON.stringify(value); } @@ -14,6 +18,7 @@ function prepareDefine(config: DotenvParseOutput | undefined) { } const define = prepareDefine(result.parsed); + try { esbuild .build({ diff --git a/apps/dokploy/migrate.ts b/apps/dokploy/migrate.ts deleted file mode 100644 index e1f52c9a3..000000000 --- a/apps/dokploy/migrate.ts +++ /dev/null @@ -1,149 +0,0 @@ -// import { drizzle } from "drizzle-orm/postgres-js"; -// import { nanoid } from "nanoid"; -// import postgres from "postgres"; -// import * as schema from "./server/db/schema"; - -// const connectionString = process.env.DATABASE_URL!; - -// const sql = postgres(connectionString, { max: 1 }); -// const db = drizzle(sql, { -// schema, -// }); - -// await db -// .transaction(async (db) => { -// const admins = await db.query.admins.findMany({ -// with: { -// auth: true, -// users: { -// with: { -// auth: true, -// }, -// }, -// }, -// }); -// for (const admin of admins) { -// const user = await db -// .insert(schema.users_temp) -// .values({ -// id: admin.adminId, -// email: admin.auth.email, -// token: admin.auth.token || "", -// emailVerified: true, -// updatedAt: new Date(), -// role: "admin", -// serverIp: admin.serverIp, -// image: admin.auth.image, -// certificateType: admin.certificateType, -// host: admin.host, -// letsEncryptEmail: admin.letsEncryptEmail, -// sshPrivateKey: admin.sshPrivateKey, -// enableDockerCleanup: admin.enableDockerCleanup, -// enableLogRotation: admin.enableLogRotation, -// enablePaidFeatures: admin.enablePaidFeatures, -// metricsConfig: admin.metricsConfig, -// cleanupCacheApplications: admin.cleanupCacheApplications, -// cleanupCacheOnPreviews: admin.cleanupCacheOnPreviews, -// cleanupCacheOnCompose: admin.cleanupCacheOnCompose, -// stripeCustomerId: admin.stripeCustomerId, -// stripeSubscriptionId: admin.stripeSubscriptionId, -// serversQuantity: admin.serversQuantity, -// }) -// .returning() -// .then((user) => user[0]); - -// await db.insert(schema.account).values({ -// providerId: "credential", -// userId: user?.id || "", -// password: admin.auth.password, -// is2FAEnabled: admin.auth.is2FAEnabled || false, -// createdAt: new Date(admin.auth.createdAt) || new Date(), -// updatedAt: new Date(admin.auth.createdAt) || new Date(), -// }); - -// const organization = await db -// .insert(schema.organization) -// .values({ -// name: "My Organization", -// slug: nanoid(), -// ownerId: user?.id || "", -// createdAt: new Date(admin.createdAt) || new Date(), -// }) -// .returning() -// .then((organization) => organization[0]); - -// for (const member of admin.users) { -// const userTemp = await db -// .insert(schema.users_temp) -// .values({ -// id: member.userId, -// email: member.auth.email, -// token: member.token || "", -// emailVerified: true, -// updatedAt: new Date(admin.createdAt) || new Date(), -// role: "user", -// image: member.auth.image, -// createdAt: admin.createdAt, -// canAccessToAPI: member.canAccessToAPI || false, -// canAccessToDocker: member.canAccessToDocker || false, -// canAccessToGitProviders: member.canAccessToGitProviders || false, -// canAccessToSSHKeys: member.canAccessToSSHKeys || false, -// canAccessToTraefikFiles: member.canAccessToTraefikFiles || false, -// canCreateProjects: member.canCreateProjects || false, -// canCreateServices: member.canCreateServices || false, -// canDeleteProjects: member.canDeleteProjects || false, -// canDeleteServices: member.canDeleteServices || false, -// accessedProjects: member.accessedProjects || [], -// accessedServices: member.accessedServices || [], -// }) -// .returning() -// .then((userTemp) => userTemp[0]); - -// await db.insert(schema.account).values({ -// providerId: "credential", -// userId: member?.userId || "", -// password: member.auth.password, -// is2FAEnabled: member.auth.is2FAEnabled || false, -// createdAt: new Date(member.auth.createdAt) || new Date(), -// updatedAt: new Date(member.auth.createdAt) || new Date(), -// }); - -// await db.insert(schema.member).values({ -// organizationId: organization?.id || "", -// userId: userTemp?.id || "", -// role: "admin", -// createdAt: new Date(member.createdAt) || new Date(), -// }); -// } -// } -// }) -// .then(() => { -// console.log("Migration finished"); -// }) -// .catch((error) => { -// console.error(error); -// }); - -// await db -// .transaction(async (db) => { -// const projects = await db.query.projects.findMany({ -// with: { -// user: { -// with: { -// organizations: true, -// }, -// }, -// }, -// }); -// for (const project of projects) { -// const _user = await db.update(schema.projects).set({ -// organizationId: project.user.organizations[0]?.id || "", -// }); -// } -// }) -// .then(() => { -// console.log("Migration finished"); -// }) -// .catch((error) => { -// console.error(error); -// }); diff --git a/apps/dokploy/server/db/index.ts b/apps/dokploy/server/db/index.ts index 3ac6e3940..55d6d3a46 100644 --- a/apps/dokploy/server/db/index.ts +++ b/apps/dokploy/server/db/index.ts @@ -6,14 +6,18 @@ declare global { var db: PostgresJsDatabase | undefined; } +const dbUrl = + process.env.DATABASE_URL || + "postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy"; + export let db: PostgresJsDatabase; if (process.env.NODE_ENV === "production") { - db = drizzle(postgres(process.env.DATABASE_URL!), { + db = drizzle(postgres(dbUrl!), { schema, }); } else { if (!global.db) - global.db = drizzle(postgres(process.env.DATABASE_URL!), { + global.db = drizzle(postgres(dbUrl!), { schema, }); diff --git a/apps/dokploy/server/db/seed.ts b/apps/dokploy/server/db/seed.ts deleted file mode 100644 index 5b3eb6c62..000000000 --- a/apps/dokploy/server/db/seed.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { drizzle } from "drizzle-orm/postgres-js"; -import postgres from "postgres"; - -const connectionString = process.env.DATABASE_URL!; - -const pg = postgres(connectionString, { max: 1 }); -const _db = drizzle(pg); - -async function seed() { - console.log("> Seed:", process.env.DATABASE_PATH, "\n"); - - // const authenticationR = await db - // .insert(users) - // .values([ - // { - // email: "user1@hotmail.com", - // password: password("12345671"), - // }, - // ]) - // .onConflictDoNothing() - // .returning(); - - // console.log("\nSemillas Update:", authenticationR.length); -} - -seed().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/packages/server/package.json b/packages/server/package.json index dbf7d3c65..3b249a65b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -111,4 +111,4 @@ "node": "^20.16.0", "pnpm": ">=9.12.0" } -} +} \ No newline at end of file diff --git a/packages/server/src/db/drizzle.config.ts b/packages/server/src/db/drizzle.config.ts deleted file mode 100644 index 60a3bb937..000000000 --- a/packages/server/src/db/drizzle.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -export default defineConfig({ - schema: "./server/db/schema/index.ts", - dialect: "postgresql", - dbCredentials: { - url: process.env.DATABASE_URL!, - }, - out: "drizzle", - migrations: { - table: "migrations", - schema: "public", - }, -}); diff --git a/packages/server/src/db/migration.ts b/packages/server/src/db/migration.ts deleted file mode 100644 index 6fada0833..000000000 --- a/packages/server/src/db/migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -// import { drizzle } from "drizzle-orm/postgres-js"; -// import { migrate } from "drizzle-orm/postgres-js/migrator"; -// import postgres from "postgres"; - -// const connectionString = process.env.DATABASE_URL!; - -// const sql = postgres(connectionString, { max: 1 }); -// const db = drizzle(sql); - -// export const migration = async () => -// await migrate(db, { migrationsFolder: "drizzle" }) -// .then(() => { -// console.log("Migration complete"); -// sql.end(); -// }) -// .catch((error) => { -// console.log("Migration failed", error); -// }) -// .finally(() => { -// sql.end(); -// }); diff --git a/packages/server/src/db/reset.ts b/packages/server/src/db/reset.ts deleted file mode 100644 index c22291478..000000000 --- a/packages/server/src/db/reset.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { sql } from "drizzle-orm"; -// Credits to Louistiti from Drizzle Discord: https://discord.com/channels/1043890932593987624/1130802621750448160/1143083373535973406 -import { drizzle } from "drizzle-orm/postgres-js"; -import postgres from "postgres"; - -const connectionString = process.env.DATABASE_URL!; - -const pg = postgres(connectionString, { max: 1 }); -const db = drizzle(pg); - -const clearDb = async (): Promise => { - try { - const tablesQuery = sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public; DROP schema drizzle CASCADE;`; - const tables = await db.execute(tablesQuery); - console.log(tables); - await pg.end(); - } catch (error) { - console.error("Error cleaning database", error); - } finally { - } -}; - -clearDb(); diff --git a/packages/server/src/db/seed.ts b/packages/server/src/db/seed.ts deleted file mode 100644 index 7e2736b00..000000000 --- a/packages/server/src/db/seed.ts +++ /dev/null @@ -1,35 +0,0 @@ -// import bc from "bcrypt"; -// import { drizzle } from "drizzle-orm/postgres-js"; -// import postgres from "postgres"; -// import { users } from "./schema"; - -// const connectionString = process.env.DATABASE_URL!; - -// const pg = postgres(connectionString, { max: 1 }); -// const db = drizzle(pg); - -// function password(txt: string) { -// return bc.hashSync(txt, 10); -// } - -// async function seed() { -// console.log("> Seed:", process.env.DATABASE_PATH, "\n"); - -// // const authenticationR = await db -// // .insert(users) -// // .values([ -// // { -// // email: "user1@hotmail.com", -// // password: password("12345671"), -// // }, -// // ]) -// // .onConflictDoNothing() -// // .returning(); - -// // console.log("\nSemillas Update:", authenticationR.length); -// } - -// seed().catch((e) => { -// console.error(e); -// process.exit(1); -// }); From 935d1686f281a4d07cde7f07ef0c89d96f517137 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 28 Aug 2025 19:02:21 -0600 Subject: [PATCH 02/65] chore: add new branch for database migration fix in Dokploy workflow --- .github/workflows/dokploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dokploy.yml b/.github/workflows/dokploy.yml index afb4ba4d2..529cd8f7f 100644 --- a/.github/workflows/dokploy.yml +++ b/.github/workflows/dokploy.yml @@ -2,7 +2,7 @@ name: Dokploy Docker Build on: push: - branches: [main, canary] + branches: [main, canary, "fix/re-apply-database-migration-fix"] workflow_dispatch: env: From caf244120cafdcac1fbaa735d4a5e9210e893944 Mon Sep 17 00:00:00 2001 From: Typed SIGTERM Date: Sat, 30 Aug 2025 13:41:40 +0800 Subject: [PATCH 03/65] fix: print error when docker build fails --- packages/server/src/utils/providers/docker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/providers/docker.ts b/packages/server/src/utils/providers/docker.ts index 88c457767..8cca44ba8 100644 --- a/packages/server/src/utils/providers/docker.ts +++ b/packages/server/src/utils/providers/docker.ts @@ -42,7 +42,7 @@ export const buildDocker = async ( await mechanizeDockerContainer(application); writeStream.write("\nDocker Deployed: ✅\n"); } catch (error) { - writeStream.write("❌ Error"); + writeStream.write("❌ ${error}"); throw error; } finally { writeStream.end(); From 468feaa092b373011ecbd101cc37b17b17064877 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Sun, 31 Aug 2025 10:25:09 +1000 Subject: [PATCH 04/65] fix(ui): improve server schedule responsiveness for mobile --- .../deployments/show-deployments.tsx | 454 +++++++++--------- .../application/schedules/show-schedules.tsx | 428 ++++++++--------- 2 files changed, 442 insertions(+), 440 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 5efa9e5f2..1dad1cf9f 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -7,11 +7,11 @@ import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { api, type RouterOutputs } from "@/utils/api"; import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings"; @@ -20,238 +20,240 @@ import { RefreshToken } from "./refresh-token"; import { ShowDeployment } from "./show-deployment"; interface Props { - id: string; - type: - | "application" - | "compose" - | "schedule" - | "server" - | "backup" - | "previewDeployment" - | "volumeBackup"; - refreshToken?: string; - serverId?: string; + id: string; + type: + | "application" + | "compose" + | "schedule" + | "server" + | "backup" + | "previewDeployment" + | "volumeBackup"; + refreshToken?: string; + serverId?: string; } export const formatDuration = (seconds: number) => { - if (seconds < 60) return `${seconds}s`; - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${minutes}m ${remainingSeconds}s`; + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}m ${remainingSeconds}s`; }; export const ShowDeployments = ({ - id, - type, - refreshToken, - serverId, + id, + type, + refreshToken, + serverId, }: Props) => { - const [activeLog, setActiveLog] = useState< - RouterOutputs["deployment"]["all"][number] | null - >(null); - const { data: deployments, isLoading: isLoadingDeployments } = - api.deployment.allByType.useQuery( - { - id, - type, - }, - { - enabled: !!id, - refetchInterval: 1000, - }, - ); + const [activeLog, setActiveLog] = useState< + RouterOutputs["deployment"]["all"][number] | null + >(null); + const { data: deployments, isLoading: isLoadingDeployments } = + api.deployment.allByType.useQuery( + { + id, + type, + }, + { + enabled: !!id, + refetchInterval: 1000, + } + ); - const { mutateAsync: rollback, isLoading: isRollingBack } = - api.rollback.rollback.useMutation(); - const { mutateAsync: killProcess, isLoading: isKillingProcess } = - api.deployment.killProcess.useMutation(); + const { mutateAsync: rollback, isLoading: isRollingBack } = + api.rollback.rollback.useMutation(); + const { mutateAsync: killProcess, isLoading: isKillingProcess } = + api.deployment.killProcess.useMutation(); - const [url, setUrl] = React.useState(""); - useEffect(() => { - setUrl(document.location.origin); - }, []); + const [url, setUrl] = React.useState(""); + useEffect(() => { + setUrl(document.location.origin); + }, []); - return ( - - -
- Deployments - - See all the 10 last deployments for this {type} - -
-
- {(type === "application" || type === "compose") && ( - - )} - {type === "application" && ( - - - - )} -
-
- - {refreshToken && ( -
- - If you want to re-deploy this application use this URL in the - config of your git provider or docker - -
- Webhook URL: -
- - {`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`} - - {(type === "application" || type === "compose") && ( - - )} -
-
-
- )} + return ( + + +
+ Deployments + + See the last 10 deployments for this {type} + +
+
+ {(type === "application" || type === "compose") && ( + + )} + {type === "application" && ( + + + + )} +
+
+ + {refreshToken && ( +
+ + If you want to re-deploy this application use this URL in the + config of your git provider or docker + +
+ Webhook URL: +
+ + {`${url}/api/deploy${ + type === "compose" ? "/compose" : "" + }/${refreshToken}`} + + {(type === "application" || type === "compose") && ( + + )} +
+
+
+ )} - {isLoadingDeployments ? ( -
- - - Loading deployments... - -
- ) : deployments?.length === 0 ? ( -
- - - No deployments found - -
- ) : ( -
- {deployments?.map((deployment, index) => ( -
-
- - {index + 1}. {deployment.status} - - - - {deployment.title} - - {deployment.description && ( - - {deployment.description} - - )} -
-
-
- - {deployment.startedAt && deployment.finishedAt && ( - - - {formatDuration( - Math.floor( - (new Date(deployment.finishedAt).getTime() - - new Date(deployment.startedAt).getTime()) / - 1000, - ), - )} - - )} -
+ {isLoadingDeployments ? ( +
+ + + Loading deployments... + +
+ ) : deployments?.length === 0 ? ( +
+ + + No deployments found + +
+ ) : ( +
+ {deployments?.map((deployment, index) => ( +
+
+ + {index + 1}. {deployment.status} + + + + {deployment.title} + + {deployment.description && ( + + {deployment.description} + + )} +
+
+
+ + {deployment.startedAt && deployment.finishedAt && ( + + + {formatDuration( + Math.floor( + (new Date(deployment.finishedAt).getTime() - + new Date(deployment.startedAt).getTime()) / + 1000 + ) + )} + + )} +
-
- {deployment.pid && deployment.status === "running" && ( - { - await killProcess({ - deploymentId: deployment.deploymentId, - }) - .then(() => { - toast.success("Process killed successfully"); - }) - .catch(() => { - toast.error("Error killing process"); - }); - }} - > - - - )} - +
+ {deployment.pid && deployment.status === "running" && ( + { + await killProcess({ + deploymentId: deployment.deploymentId, + }) + .then(() => { + toast.success("Process killed successfully"); + }) + .catch(() => { + toast.error("Error killing process"); + }); + }} + > + + + )} + - {deployment?.rollback && - deployment.status === "done" && - type === "application" && ( - { - await rollback({ - rollbackId: deployment.rollback.rollbackId, - }) - .then(() => { - toast.success( - "Rollback initiated successfully", - ); - }) - .catch(() => { - toast.error("Error initiating rollback"); - }); - }} - > - - - )} -
-
-
- ))} -
- )} - setActiveLog(null)} - logPath={activeLog?.logPath || ""} - errorMessage={activeLog?.errorMessage || ""} - /> - - - ); + {deployment?.rollback && + deployment.status === "done" && + type === "application" && ( + { + await rollback({ + rollbackId: deployment.rollback.rollbackId, + }) + .then(() => { + toast.success( + "Rollback initiated successfully" + ); + }) + .catch(() => { + toast.error("Error initiating rollback"); + }); + }} + > + + + )} +
+
+
+ ))} +
+ )} + setActiveLog(null)} + logPath={activeLog?.logPath || ""} + errorMessage={activeLog?.errorMessage || ""} + /> +
+
+ ); }; diff --git a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx index 3ebc76def..339815343 100644 --- a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx +++ b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx @@ -1,243 +1,243 @@ import { - ClipboardList, - Clock, - Loader2, - Play, - Terminal, - Trash2, + ClipboardList, + Clock, + Loader2, + Play, + Terminal, + Trash2, } from "lucide-react"; import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; import { HandleSchedules } from "./handle-schedules"; interface Props { - id: string; - scheduleType?: "application" | "compose" | "server" | "dokploy-server"; + id: string; + scheduleType?: "application" | "compose" | "server" | "dokploy-server"; } export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => { - const { - data: schedules, - isLoading: isLoadingSchedules, - refetch: refetchSchedules, - } = api.schedule.list.useQuery( - { - id: id || "", - scheduleType, - }, - { - enabled: !!id, - }, - ); + const { + data: schedules, + isLoading: isLoadingSchedules, + refetch: refetchSchedules, + } = api.schedule.list.useQuery( + { + id: id || "", + scheduleType, + }, + { + enabled: !!id, + } + ); - const utils = api.useUtils(); + const utils = api.useUtils(); - const { mutateAsync: deleteSchedule, isLoading: isDeleting } = - api.schedule.delete.useMutation(); + const { mutateAsync: deleteSchedule, isLoading: isDeleting } = + api.schedule.delete.useMutation(); - const { mutateAsync: runManually, isLoading } = - api.schedule.runManually.useMutation(); + const { mutateAsync: runManually, isLoading } = + api.schedule.runManually.useMutation(); - return ( - - -
-
- - Scheduled Tasks - - - Schedule tasks to run automatically at specified intervals. - -
+ return ( + + +
+
+ + Scheduled Tasks + + + Schedule tasks to run automatically at specified intervals. + +
- {schedules && schedules.length > 0 && ( - - )} -
-
- - {isLoadingSchedules ? ( -
- - - Loading scheduled tasks... - -
- ) : schedules && schedules.length > 0 ? ( -
- {schedules.map((schedule) => { - const serverId = - schedule.serverId || - schedule.application?.serverId || - schedule.compose?.serverId; - return ( -
-
-
- -
-
-
-

- {schedule.name} -

- - {schedule.enabled ? "Enabled" : "Disabled"} - -
-
- - Cron: {schedule.cronExpression} - - {schedule.scheduleType !== "server" && - schedule.scheduleType !== "dokploy-server" && ( - <> - - • - - - {schedule.shellType} - - - )} -
- {schedule.command && ( -
- - - {schedule.command} - -
- )} -
-
+ {schedules && schedules.length > 0 && ( + + )} +
+ + + {isLoadingSchedules ? ( +
+ + + Loading scheduled tasks... + +
+ ) : schedules && schedules.length > 0 ? ( +
+ {schedules.map((schedule) => { + const serverId = + schedule.serverId || + schedule.application?.serverId || + schedule.compose?.serverId; + return ( +
+
+
+ +
+
+
+

+ {schedule.name} +

+ + {schedule.enabled ? "Enabled" : "Disabled"} + +
+
+ + Cron: {schedule.cronExpression} + + {schedule.scheduleType !== "server" && + schedule.scheduleType !== "dokploy-server" && ( + <> + + • + + + {schedule.shellType} + + + )} +
+ {schedule.command && ( +
+ + + {schedule.command} + +
+ )} +
+
-
- - - +
+ + + - - - - - - Run Manual Schedule - - + await runManually({ + scheduleId: schedule.scheduleId, + }) + .then(async () => { + await new Promise((resolve) => + setTimeout(resolve, 1500) + ); + refetchSchedules(); + }) + .catch(() => { + toast.error("Error running schedule"); + }); + }} + > + + + + Run Manual Schedule + + - + - { - await deleteSchedule({ - scheduleId: schedule.scheduleId, - }) - .then(() => { - utils.schedule.list.invalidate({ - id, - scheduleType, - }); - toast.success("Schedule deleted successfully"); - }) - .catch(() => { - toast.error("Error deleting schedule"); - }); - }} - > - - -
-
- ); - })} -
- ) : ( -
- -

- No scheduled tasks -

-

- Create your first scheduled task to automate your workflows -

- -
- )} - - - ); + { + await deleteSchedule({ + scheduleId: schedule.scheduleId, + }) + .then(() => { + utils.schedule.list.invalidate({ + id, + scheduleType, + }); + toast.success("Schedule deleted successfully"); + }) + .catch(() => { + toast.error("Error deleting schedule"); + }); + }} + > + + +
+
+ ); + })} +
+ ) : ( +
+ +

+ No scheduled tasks +

+

+ Create your first scheduled task to automate your workflows +

+ +
+ )} +
+
+ ); }; From a2841fdd30dce6f61c85513c3cad07e660a3a978 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Sun, 31 Aug 2025 10:27:12 +1000 Subject: [PATCH 05/65] fix(ui): flex-wrap for cron and shell type --- .../dashboard/application/schedules/show-schedules.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx index 339815343..d235f6bc5 100644 --- a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx +++ b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx @@ -109,7 +109,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => { {schedule.enabled ? "Enabled" : "Disabled"} -
+
Date: Sun, 31 Aug 2025 00:30:08 +0000 Subject: [PATCH 06/65] [autofix.ci] apply automated fixes --- .../deployments/show-deployments.tsx | 456 +++++++++--------- .../application/schedules/show-schedules.tsx | 428 ++++++++-------- 2 files changed, 442 insertions(+), 442 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 1dad1cf9f..13694a283 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -7,11 +7,11 @@ import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { api, type RouterOutputs } from "@/utils/api"; import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings"; @@ -20,240 +20,240 @@ import { RefreshToken } from "./refresh-token"; import { ShowDeployment } from "./show-deployment"; interface Props { - id: string; - type: - | "application" - | "compose" - | "schedule" - | "server" - | "backup" - | "previewDeployment" - | "volumeBackup"; - refreshToken?: string; - serverId?: string; + id: string; + type: + | "application" + | "compose" + | "schedule" + | "server" + | "backup" + | "previewDeployment" + | "volumeBackup"; + refreshToken?: string; + serverId?: string; } export const formatDuration = (seconds: number) => { - if (seconds < 60) return `${seconds}s`; - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${minutes}m ${remainingSeconds}s`; + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}m ${remainingSeconds}s`; }; export const ShowDeployments = ({ - id, - type, - refreshToken, - serverId, + id, + type, + refreshToken, + serverId, }: Props) => { - const [activeLog, setActiveLog] = useState< - RouterOutputs["deployment"]["all"][number] | null - >(null); - const { data: deployments, isLoading: isLoadingDeployments } = - api.deployment.allByType.useQuery( - { - id, - type, - }, - { - enabled: !!id, - refetchInterval: 1000, - } - ); + const [activeLog, setActiveLog] = useState< + RouterOutputs["deployment"]["all"][number] | null + >(null); + const { data: deployments, isLoading: isLoadingDeployments } = + api.deployment.allByType.useQuery( + { + id, + type, + }, + { + enabled: !!id, + refetchInterval: 1000, + }, + ); - const { mutateAsync: rollback, isLoading: isRollingBack } = - api.rollback.rollback.useMutation(); - const { mutateAsync: killProcess, isLoading: isKillingProcess } = - api.deployment.killProcess.useMutation(); + const { mutateAsync: rollback, isLoading: isRollingBack } = + api.rollback.rollback.useMutation(); + const { mutateAsync: killProcess, isLoading: isKillingProcess } = + api.deployment.killProcess.useMutation(); - const [url, setUrl] = React.useState(""); - useEffect(() => { - setUrl(document.location.origin); - }, []); + const [url, setUrl] = React.useState(""); + useEffect(() => { + setUrl(document.location.origin); + }, []); - return ( - - -
- Deployments - - See the last 10 deployments for this {type} - -
-
- {(type === "application" || type === "compose") && ( - - )} - {type === "application" && ( - - - - )} -
-
- - {refreshToken && ( -
- - If you want to re-deploy this application use this URL in the - config of your git provider or docker - -
- Webhook URL: -
- - {`${url}/api/deploy${ - type === "compose" ? "/compose" : "" - }/${refreshToken}`} - - {(type === "application" || type === "compose") && ( - - )} -
-
-
- )} + return ( + + +
+ Deployments + + See the last 10 deployments for this {type} + +
+
+ {(type === "application" || type === "compose") && ( + + )} + {type === "application" && ( + + + + )} +
+
+ + {refreshToken && ( +
+ + If you want to re-deploy this application use this URL in the + config of your git provider or docker + +
+ Webhook URL: +
+ + {`${url}/api/deploy${ + type === "compose" ? "/compose" : "" + }/${refreshToken}`} + + {(type === "application" || type === "compose") && ( + + )} +
+
+
+ )} - {isLoadingDeployments ? ( -
- - - Loading deployments... - -
- ) : deployments?.length === 0 ? ( -
- - - No deployments found - -
- ) : ( -
- {deployments?.map((deployment, index) => ( -
-
- - {index + 1}. {deployment.status} - - - - {deployment.title} - - {deployment.description && ( - - {deployment.description} - - )} -
-
-
- - {deployment.startedAt && deployment.finishedAt && ( - - - {formatDuration( - Math.floor( - (new Date(deployment.finishedAt).getTime() - - new Date(deployment.startedAt).getTime()) / - 1000 - ) - )} - - )} -
+ {isLoadingDeployments ? ( +
+ + + Loading deployments... + +
+ ) : deployments?.length === 0 ? ( +
+ + + No deployments found + +
+ ) : ( +
+ {deployments?.map((deployment, index) => ( +
+
+ + {index + 1}. {deployment.status} + + + + {deployment.title} + + {deployment.description && ( + + {deployment.description} + + )} +
+
+
+ + {deployment.startedAt && deployment.finishedAt && ( + + + {formatDuration( + Math.floor( + (new Date(deployment.finishedAt).getTime() - + new Date(deployment.startedAt).getTime()) / + 1000, + ), + )} + + )} +
-
- {deployment.pid && deployment.status === "running" && ( - { - await killProcess({ - deploymentId: deployment.deploymentId, - }) - .then(() => { - toast.success("Process killed successfully"); - }) - .catch(() => { - toast.error("Error killing process"); - }); - }} - > - - - )} - +
+ {deployment.pid && deployment.status === "running" && ( + { + await killProcess({ + deploymentId: deployment.deploymentId, + }) + .then(() => { + toast.success("Process killed successfully"); + }) + .catch(() => { + toast.error("Error killing process"); + }); + }} + > + + + )} + - {deployment?.rollback && - deployment.status === "done" && - type === "application" && ( - { - await rollback({ - rollbackId: deployment.rollback.rollbackId, - }) - .then(() => { - toast.success( - "Rollback initiated successfully" - ); - }) - .catch(() => { - toast.error("Error initiating rollback"); - }); - }} - > - - - )} -
-
-
- ))} -
- )} - setActiveLog(null)} - logPath={activeLog?.logPath || ""} - errorMessage={activeLog?.errorMessage || ""} - /> - - - ); + {deployment?.rollback && + deployment.status === "done" && + type === "application" && ( + { + await rollback({ + rollbackId: deployment.rollback.rollbackId, + }) + .then(() => { + toast.success( + "Rollback initiated successfully", + ); + }) + .catch(() => { + toast.error("Error initiating rollback"); + }); + }} + > + + + )} +
+
+
+ ))} +
+ )} + setActiveLog(null)} + logPath={activeLog?.logPath || ""} + errorMessage={activeLog?.errorMessage || ""} + /> +
+
+ ); }; diff --git a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx index d235f6bc5..6369b7fdf 100644 --- a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx +++ b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx @@ -1,243 +1,243 @@ import { - ClipboardList, - Clock, - Loader2, - Play, - Terminal, - Trash2, + ClipboardList, + Clock, + Loader2, + Play, + Terminal, + Trash2, } from "lucide-react"; import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; import { HandleSchedules } from "./handle-schedules"; interface Props { - id: string; - scheduleType?: "application" | "compose" | "server" | "dokploy-server"; + id: string; + scheduleType?: "application" | "compose" | "server" | "dokploy-server"; } export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => { - const { - data: schedules, - isLoading: isLoadingSchedules, - refetch: refetchSchedules, - } = api.schedule.list.useQuery( - { - id: id || "", - scheduleType, - }, - { - enabled: !!id, - } - ); + const { + data: schedules, + isLoading: isLoadingSchedules, + refetch: refetchSchedules, + } = api.schedule.list.useQuery( + { + id: id || "", + scheduleType, + }, + { + enabled: !!id, + }, + ); - const utils = api.useUtils(); + const utils = api.useUtils(); - const { mutateAsync: deleteSchedule, isLoading: isDeleting } = - api.schedule.delete.useMutation(); + const { mutateAsync: deleteSchedule, isLoading: isDeleting } = + api.schedule.delete.useMutation(); - const { mutateAsync: runManually, isLoading } = - api.schedule.runManually.useMutation(); + const { mutateAsync: runManually, isLoading } = + api.schedule.runManually.useMutation(); - return ( - - -
-
- - Scheduled Tasks - - - Schedule tasks to run automatically at specified intervals. - -
+ return ( + + +
+
+ + Scheduled Tasks + + + Schedule tasks to run automatically at specified intervals. + +
- {schedules && schedules.length > 0 && ( - - )} -
-
- - {isLoadingSchedules ? ( -
- - - Loading scheduled tasks... - -
- ) : schedules && schedules.length > 0 ? ( -
- {schedules.map((schedule) => { - const serverId = - schedule.serverId || - schedule.application?.serverId || - schedule.compose?.serverId; - return ( -
-
-
- -
-
-
-

- {schedule.name} -

- - {schedule.enabled ? "Enabled" : "Disabled"} - -
-
- - Cron: {schedule.cronExpression} - - {schedule.scheduleType !== "server" && - schedule.scheduleType !== "dokploy-server" && ( - <> - - • - - - {schedule.shellType} - - - )} -
- {schedule.command && ( -
- - - {schedule.command} - -
- )} -
-
+ {schedules && schedules.length > 0 && ( + + )} +
+ + + {isLoadingSchedules ? ( +
+ + + Loading scheduled tasks... + +
+ ) : schedules && schedules.length > 0 ? ( +
+ {schedules.map((schedule) => { + const serverId = + schedule.serverId || + schedule.application?.serverId || + schedule.compose?.serverId; + return ( +
+
+
+ +
+
+
+

+ {schedule.name} +

+ + {schedule.enabled ? "Enabled" : "Disabled"} + +
+
+ + Cron: {schedule.cronExpression} + + {schedule.scheduleType !== "server" && + schedule.scheduleType !== "dokploy-server" && ( + <> + + • + + + {schedule.shellType} + + + )} +
+ {schedule.command && ( +
+ + + {schedule.command} + +
+ )} +
+
-
- - - +
+ + + - - - - - - Run Manual Schedule - - + await runManually({ + scheduleId: schedule.scheduleId, + }) + .then(async () => { + await new Promise((resolve) => + setTimeout(resolve, 1500), + ); + refetchSchedules(); + }) + .catch(() => { + toast.error("Error running schedule"); + }); + }} + > + + + + Run Manual Schedule + + - + - { - await deleteSchedule({ - scheduleId: schedule.scheduleId, - }) - .then(() => { - utils.schedule.list.invalidate({ - id, - scheduleType, - }); - toast.success("Schedule deleted successfully"); - }) - .catch(() => { - toast.error("Error deleting schedule"); - }); - }} - > - - -
-
- ); - })} -
- ) : ( -
- -

- No scheduled tasks -

-

- Create your first scheduled task to automate your workflows -

- -
- )} - - - ); + { + await deleteSchedule({ + scheduleId: schedule.scheduleId, + }) + .then(() => { + utils.schedule.list.invalidate({ + id, + scheduleType, + }); + toast.success("Schedule deleted successfully"); + }) + .catch(() => { + toast.error("Error deleting schedule"); + }); + }} + > + + +
+
+ ); + })} +
+ ) : ( +
+ +

+ No scheduled tasks +

+

+ Create your first scheduled task to automate your workflows +

+ +
+ )} +
+
+ ); }; From 38abe032574c1e5468d588c9e0fa35283b96e045 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Sun, 31 Aug 2025 10:36:07 +1000 Subject: [PATCH 07/65] fix(ui): flex-wrap on schedule name and enabled --- .../dashboard/application/schedules/show-schedules.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx index 6369b7fdf..3209b6e03 100644 --- a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx +++ b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx @@ -98,7 +98,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
-
+

{schedule.name}

@@ -226,7 +226,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => { })}
) : ( -
+

No scheduled tasks From bc2b4f13695ca690309c09948de8f5abd414defe Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:16:55 -0600 Subject: [PATCH 08/65] feat(database): enhance password validation for database schemas and update input components for password visibility --- .../dashboard/project/add-database.tsx | 22 ++++++++-- .../shared/toggle-visibility-input.tsx | 19 +++------ apps/dokploy/components/ui/input.tsx | 40 ++++++++++++++----- packages/server/src/db/schema/mariadb.ts | 16 +++++++- packages/server/src/db/schema/mongo.ts | 8 +++- packages/server/src/db/schema/mysql.ts | 16 +++++++- packages/server/src/db/schema/postgres.ts | 8 +++- 7 files changed, 97 insertions(+), 32 deletions(-) diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index 6b07baa81..104413908 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -83,7 +83,13 @@ const baseDatabaseSchema = z.object({ message: "App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'", }), - databasePassword: z.string(), + databasePassword: z + .string() + .min(1, "Password is required") + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }), dockerImage: z.string(), description: z.string().nullable(), serverId: z.string().nullable(), @@ -112,7 +118,12 @@ const mySchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("mysql"), - databaseRootPassword: z.string().default(""), + databaseRootPassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }), databaseUser: z.string().default("mysql"), databaseName: z.string().default("mysql"), }) @@ -121,7 +132,12 @@ const mySchema = z.discriminatedUnion("type", [ .object({ type: z.literal("mariadb"), dockerImage: z.string().default("mariadb:4"), - databaseRootPassword: z.string().default(""), + databaseRootPassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }), databaseUser: z.string().default("mariadb"), databaseName: z.string().default("mariadb"), }) diff --git a/apps/dokploy/components/shared/toggle-visibility-input.tsx b/apps/dokploy/components/shared/toggle-visibility-input.tsx index 1a9817379..aea173fbc 100644 --- a/apps/dokploy/components/shared/toggle-visibility-input.tsx +++ b/apps/dokploy/components/shared/toggle-visibility-input.tsx @@ -1,25 +1,16 @@ import copy from "copy-to-clipboard"; -import { Clipboard, EyeIcon, EyeOffIcon } from "lucide-react"; -import { useRef, useState } from "react"; +import { Clipboard } from "lucide-react"; +import { useRef } from "react"; import { toast } from "sonner"; import { Button } from "../ui/button"; import { Input, type InputProps } from "../ui/input"; export const ToggleVisibilityInput = ({ ...props }: InputProps) => { - const [isPasswordVisible, setIsPasswordVisible] = useState(false); const inputRef = useRef(null); - const togglePasswordVisibility = () => { - setIsPasswordVisible((prevVisibility) => !prevVisibility); - }; - return (

- + - + */}
); }; diff --git a/apps/dokploy/components/ui/input.tsx b/apps/dokploy/components/ui/input.tsx index df3ca4d07..87afd64e5 100644 --- a/apps/dokploy/components/ui/input.tsx +++ b/apps/dokploy/components/ui/input.tsx @@ -1,3 +1,4 @@ +import { EyeIcon, EyeOffIcon } from "lucide-react"; import * as React from "react"; import { cn } from "@/lib/utils"; @@ -8,18 +9,39 @@ export interface InputProps const Input = React.forwardRef( ({ className, errorMessage, type, ...props }, ref) => { + const [showPassword, setShowPassword] = React.useState(false); + const isPassword = type === "password"; + const inputType = isPassword ? (showPassword ? "text" : "password") : type; + return ( <> - + + {isPassword && ( + )} - ref={ref} - {...props} - /> +
{errorMessage && ( {errorMessage} diff --git a/packages/server/src/db/schema/mariadb.ts b/packages/server/src/db/schema/mariadb.ts index 039836d77..83fa94f14 100644 --- a/packages/server/src/db/schema/mariadb.ts +++ b/packages/server/src/db/schema/mariadb.ts @@ -94,8 +94,20 @@ const createSchema = createInsertSchema(mariadb, { createdAt: z.string(), databaseName: z.string().min(1), databaseUser: z.string().min(1), - databasePassword: z.string(), - databaseRootPassword: z.string().optional(), + databasePassword: z + .string() + .min(1, "Password is required") + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }), + databaseRootPassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }) + .optional(), dockerImage: z.string().default("mariadb:6"), command: z.string().optional(), env: z.string().optional(), diff --git a/packages/server/src/db/schema/mongo.ts b/packages/server/src/db/schema/mongo.ts index eb6103a36..fed414bf6 100644 --- a/packages/server/src/db/schema/mongo.ts +++ b/packages/server/src/db/schema/mongo.ts @@ -89,7 +89,13 @@ const createSchema = createInsertSchema(mongo, { createdAt: z.string(), mongoId: z.string(), name: z.string().min(1), - databasePassword: z.string(), + databasePassword: z + .string() + .min(1, "Password is required") + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }), databaseUser: z.string().min(1), dockerImage: z.string().default("mongo:15"), command: z.string().optional(), diff --git a/packages/server/src/db/schema/mysql.ts b/packages/server/src/db/schema/mysql.ts index 03d360b3d..361d5685d 100644 --- a/packages/server/src/db/schema/mysql.ts +++ b/packages/server/src/db/schema/mysql.ts @@ -92,8 +92,20 @@ const createSchema = createInsertSchema(mysql, { name: z.string().min(1), databaseName: z.string().min(1), databaseUser: z.string().min(1), - databasePassword: z.string(), - databaseRootPassword: z.string().optional(), + databasePassword: z + .string() + .min(1, "Password is required") + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }), + databaseRootPassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }) + .optional(), dockerImage: z.string().default("mysql:8"), command: z.string().optional(), env: z.string().optional(), diff --git a/packages/server/src/db/schema/postgres.ts b/packages/server/src/db/schema/postgres.ts index df0202094..66a58d5c6 100644 --- a/packages/server/src/db/schema/postgres.ts +++ b/packages/server/src/db/schema/postgres.ts @@ -88,7 +88,13 @@ export const postgresRelations = relations(postgres, ({ one, many }) => ({ const createSchema = createInsertSchema(postgres, { postgresId: z.string(), name: z.string().min(1), - databasePassword: z.string(), + databasePassword: z + .string() + .min(1, "Password is required") + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }), databaseName: z.string().min(1), databaseUser: z.string().min(1), dockerImage: z.string().default("postgres:15"), From 5e1a164a54d43b7d6359258658e9f8e88f31a457 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:19:24 -0600 Subject: [PATCH 09/65] chore(pr-template): streamline checklist formatting and clarify issue closing instructions --- .github/pull_request_template.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 58825f900..0b849afc0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,16 +6,13 @@ Please describe in a short paragraph what this PR is about. Before submitting this PR, please make sure that: -- [ ] You created a dedicated branch based on the `canary` branch. -- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request -- [ ] You have tested this PR in your local instance. +- [] You created a dedicated branch based on the `canary` branch. +- [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request +- [] You have tested this PR in your local instance. ## Issues related (if applicable) -Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER` - -Example: `closes #123` +closes #123 ## Screenshots (if applicable) -If you include a video or screenshot, would be awesome so we can see the changes in action. \ No newline at end of file From 6fc325fe95a7f29979e740e481bc830675e2cd88 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:36:27 -0600 Subject: [PATCH 10/65] feat(environment): implement environment management with create, duplicate, and delete functionalities; add environment schema and database migrations --- .../project/environment-management.tsx | 387 + .../dokploy/drizzle/0107_charming_chimera.sql | 26 + .../drizzle/0108_keen_doctor_faustus.sql | 111 + apps/dokploy/drizzle/meta/0107_snapshot.json | 6481 ++++++++++++++++ apps/dokploy/drizzle/meta/0108_snapshot.json | 6595 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 14 + .../pages/dashboard/project/[projectId].tsx | 5 +- apps/dokploy/server/api/root.ts | 2 + .../dokploy/server/api/routers/environment.ts | 113 + apps/dokploy/server/api/routers/project.ts | 144 +- packages/server/src/db/schema/application.ts | 10 + packages/server/src/db/schema/compose.ts | 11 + packages/server/src/db/schema/environment.ts | 84 + packages/server/src/db/schema/index.ts | 1 + packages/server/src/db/schema/mariadb.ts | 10 + packages/server/src/db/schema/mongo.ts | 10 + packages/server/src/db/schema/mysql.ts | 9 + packages/server/src/db/schema/postgres.ts | 10 + packages/server/src/db/schema/project.ts | 16 +- packages/server/src/db/schema/redis.ts | 10 + packages/server/src/index.ts | 1 + packages/server/src/services/environment.ts | 113 + packages/server/src/services/project.ts | 27 +- 23 files changed, 14108 insertions(+), 82 deletions(-) create mode 100644 apps/dokploy/components/dashboard/project/environment-management.tsx create mode 100644 apps/dokploy/drizzle/0107_charming_chimera.sql create mode 100644 apps/dokploy/drizzle/0108_keen_doctor_faustus.sql create mode 100644 apps/dokploy/drizzle/meta/0107_snapshot.json create mode 100644 apps/dokploy/drizzle/meta/0108_snapshot.json create mode 100644 apps/dokploy/server/api/routers/environment.ts create mode 100644 packages/server/src/db/schema/environment.ts create mode 100644 packages/server/src/services/environment.ts diff --git a/apps/dokploy/components/dashboard/project/environment-management.tsx b/apps/dokploy/components/dashboard/project/environment-management.tsx new file mode 100644 index 000000000..929d1f3e1 --- /dev/null +++ b/apps/dokploy/components/dashboard/project/environment-management.tsx @@ -0,0 +1,387 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Copy, Plus, Settings, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { DateTooltip } from "@/components/shared/date-tooltip"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { api } from "@/utils/api"; + +const createEnvironmentSchema = z.object({ + name: z.string().min(1, "Environment name is required"), + description: z.string().optional(), +}); + +const duplicateEnvironmentSchema = z.object({ + name: z.string().min(1, "Environment name is required"), + description: z.string().optional(), +}); + +type CreateEnvironment = z.infer; +type DuplicateEnvironment = z.infer; + +interface Props { + projectId: string; + children?: React.ReactNode; +} + +export const EnvironmentManagement = ({ projectId, children }: Props) => { + const [isOpen, setIsOpen] = useState(false); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [isDuplicateDialogOpen, setIsDuplicateDialogOpen] = useState(false); + const [selectedEnvironmentId, setSelectedEnvironmentId] = + useState(""); + + const utils = api.useUtils(); + + // Queries + const { data: environments, isLoading: environmentsLoading } = + api.environment.byProjectId.useQuery( + { projectId }, + { enabled: !!projectId }, + ); + + // Mutations + const createEnvironmentMutation = api.environment.create.useMutation(); + const duplicateEnvironmentMutation = api.environment.duplicate.useMutation(); + const deleteEnvironmentMutation = api.environment.remove.useMutation(); + + // Forms + const createForm = useForm({ + defaultValues: { + name: "", + description: "", + }, + resolver: zodResolver(createEnvironmentSchema), + }); + + const duplicateForm = useForm({ + defaultValues: { + name: "", + description: "", + }, + resolver: zodResolver(duplicateEnvironmentSchema), + }); + + const onCreateSubmit = async (formData: CreateEnvironment) => { + try { + await createEnvironmentMutation.mutateAsync({ + ...formData, + projectId, + }); + toast.success("Environment created successfully"); + utils.environment.byProjectId.invalidate({ projectId }); + setIsCreateDialogOpen(false); + createForm.reset(); + } catch (error) { + toast.error("Error creating environment"); + } + }; + + const onDuplicateSubmit = async (formData: DuplicateEnvironment) => { + if (!selectedEnvironmentId) return; + + try { + await duplicateEnvironmentMutation.mutateAsync({ + environmentId: selectedEnvironmentId, + name: formData.name, + description: formData.description, + }); + toast.success("Environment duplicated successfully"); + utils.environment.byProjectId.invalidate({ projectId }); + setIsDuplicateDialogOpen(false); + duplicateForm.reset(); + setSelectedEnvironmentId(""); + } catch (error) { + toast.error("Error duplicating environment"); + } + }; + + const handleDeleteEnvironment = async (environmentId: string) => { + try { + await deleteEnvironmentMutation.mutateAsync({ environmentId }); + toast.success("Environment deleted successfully"); + utils.environment.byProjectId.invalidate({ projectId }); + } catch (error) { + toast.error("Error deleting environment"); + } + }; + + const handleDuplicateClick = ( + environmentId: string, + environmentName: string, + ) => { + setSelectedEnvironmentId(environmentId); + duplicateForm.setValue("name", `${environmentName} (copy)`); + setIsDuplicateDialogOpen(true); + }; + + return ( + <> + + + {children ?? ( + + )} + + + + Environment Management + + Manage project environments. Each environment can have its own + configuration and settings. + + + +
+
+

Project Environments

+ +
+ + {environmentsLoading ? ( +
+
+ Loading environments... +
+
+ ) : environments && environments.length > 0 ? ( +
+ {environments.map((env) => ( +
+
+
+

{env.name}

+ {env.name === "production" && ( + Default + )} +
+ {env.description && ( +

+ {env.description} +

+ )} + + + Created + + +
+
+ + {env.name !== "production" && ( + + handleDeleteEnvironment(env.environmentId) + } + > + + + )} +
+
+ ))} +
+ ) : ( +
+
+ No environments found. The production environment should be + created automatically. +
+
+ )} +
+
+
+ + {/* Create Environment Dialog */} + + + + Create New Environment + + Create a new environment for this project. + + + +
+ + ( + + Environment Name + + + + + + )} + /> + + ( + + Description (Optional) + +