From 94c947e288d098b0d54aa5b015d62de96a0699b4 Mon Sep 17 00:00:00 2001 From: vicke4 Date: Thu, 3 Apr 2025 11:41:21 +0530 Subject: [PATCH 01/19] fix(backups): web-server backups auto-deletion --- packages/server/src/utils/backups/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index b83d8279..e7510def 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -106,8 +106,8 @@ export const keepLatestNBackups = async ( backup.prefix, ); - // --include "*.sql.gz" ensures nothing else other than the db backup files are touched by rclone - const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*.sql.gz" ${backupFilesPath}`; + // --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone + const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? '.zip' : '.sql.gz'}" ${backupFilesPath}`; // when we pipe the above command with this one, we only get the list of files we want to delete const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`; // this command deletes the files From e176def5b67ca74846fccb364a11b7bf2d1b9012 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 06:12:08 +0000 Subject: [PATCH 02/19] [autofix.ci] apply automated fixes --- packages/server/src/utils/backups/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index e7510def..6c940406 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -107,7 +107,7 @@ export const keepLatestNBackups = async ( ); // --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone - const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? '.zip' : '.sql.gz'}" ${backupFilesPath}`; + const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`; // when we pipe the above command with this one, we only get the list of files we want to delete const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`; // this command deletes the files From b9de05015ff14a1f631a37225cadf15eaffd34db Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 3 Apr 2025 00:17:09 -0600 Subject: [PATCH 03/19] fix(templates): add optional chaining to prevent errors when accessing template properties --- .../dashboard/project/add-template.tsx | 42 ++++++++++--------- packages/server/src/templates/processors.ts | 23 ++++++++-- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx index 5dbbcd1d..8e9de54d 100644 --- a/apps/dokploy/components/dashboard/project/add-template.tsx +++ b/apps/dokploy/components/dashboard/project/add-template.tsx @@ -307,7 +307,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { > {templates?.map((template) => (
{ )} > - {template.version} + {template?.version}
{ )} > {template.name}
- {template.name} + {template?.name} {viewMode === "detailed" && - template.tags.length > 0 && ( + template?.tags?.length > 0 && (
- {template.tags.map((tag) => ( + {template?.tags?.map((tag) => ( { {viewMode === "detailed" && (
- {template.description} + {template?.description}
)} @@ -372,25 +372,27 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { > {viewMode === "detailed" && (
- - - - {template.links.website && ( + {template?.links?.github && ( + + + )} + {template?.links?.website && ( + )} - {template.links.docs && ( + {template?.links?.docs && ( @@ -419,7 +421,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { This will create an application from the{" "} - {template.name} template and add it to your + {template?.name} template and add it to your project. diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index 86d3cdf7..7ef774f8 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -70,7 +70,7 @@ function processValue( schema: Schema, ): string { // First replace utility functions - let processedValue = value.replace(/\${([^}]+)}/g, (match, varName) => { + let processedValue = value?.replace(/\${([^}]+)}/g, (match, varName) => { // Handle utility functions if (varName === "domain") { return generateRandomDomain(schema); @@ -177,7 +177,14 @@ export function processDomains( variables: Record, schema: Schema, ): Template["domains"] { - if (!template?.config?.domains) return []; + if ( + !template?.config?.domains || + template.config.domains.length === 0 || + template.config.domains.every((domain) => !domain.serviceName) + ) { + return []; + } + return template?.config?.domains?.map((domain: DomainConfig) => ({ ...domain, host: domain.host @@ -194,7 +201,9 @@ export function processEnvVars( variables: Record, schema: Schema, ): Template["envs"] { - if (!template?.config?.env) return []; + if (!template?.config?.env || Object.keys(template.config.env).length === 0) { + return []; + } // Handle array of env vars if (Array.isArray(template.config.env)) { @@ -233,7 +242,13 @@ export function processMounts( variables: Record, schema: Schema, ): Template["mounts"] { - if (!template?.config?.mounts) return []; + if ( + !template?.config?.mounts || + template.config.mounts.length === 0 || + template.config.mounts.every((mount) => !mount.filePath && !mount.content) + ) { + return []; + } return template?.config?.mounts?.map((mount: MountConfig) => ({ filePath: processValue(mount.filePath, variables, schema), From 9a839de022fac468287934c35e2ded3bbda7defb Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 3 Apr 2025 00:22:29 -0600 Subject: [PATCH 04/19] feat(templates): add username and email generation using faker --- packages/server/src/templates/processors.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index 7ef774f8..31e7861a 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -1,3 +1,4 @@ +import { faker } from "@faker-js/faker"; import type { Schema } from "./index"; import { generateBase64, @@ -117,6 +118,14 @@ function processValue( return generateJwt(length); } + if (varName === "username") { + return faker.internet.userName().toLowerCase(); + } + + if (varName === "email") { + return faker.internet.email().toLowerCase(); + } + // If not a utility function, try to get from variables return variables[varName] || match; }); From 031d0ce315989cd4a12b6c9ab016871c8ac942c3 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 3 Apr 2025 00:22:57 -0600 Subject: [PATCH 05/19] Update dokploy version to v0.21.3 in package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 028ad20d..34ca86fa 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.21.2", + "version": "v0.21.3", "private": true, "license": "Apache-2.0", "type": "module", From 8479f20205a5cca94d7cd9e021ea04aab713fd8f Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 4 Apr 2025 01:27:53 -0600 Subject: [PATCH 06/19] feat(handle-project): enhance project name validation to disallow starting with a number --- .../dashboard/projects/handle-project.tsx | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/apps/dokploy/components/dashboard/projects/handle-project.tsx b/apps/dokploy/components/dashboard/projects/handle-project.tsx index 85b8aea9..dcb81241 100644 --- a/apps/dokploy/components/dashboard/projects/handle-project.tsx +++ b/apps/dokploy/components/dashboard/projects/handle-project.tsx @@ -31,9 +31,14 @@ import { toast } from "sonner"; import { z } from "zod"; const AddProjectSchema = z.object({ - name: z.string().min(1, { - message: "Name is required", - }), + name: z + .string() + .min(1, { + message: "Name is required", + }) + .regex(/^[a-zA-Z]/, { + message: "Project name cannot start with a number", + }), description: z.string().optional(), }); @@ -97,18 +102,6 @@ export const HandleProject = ({ projectId }: Props) => { ); }); }; - // useEffect(() => { - // const getUsers = async () => { - // const users = await authClient.admin.listUsers({ - // query: { - // limit: 100, - // }, - // }); - // console.log(users); - // }; - - // getUsers(); - // }); return ( From 36172491a41dc85d2c9372a6938b25fe7a4defc7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 4 Apr 2025 01:55:29 -0600 Subject: [PATCH 07/19] refactor(websocket): streamline WebSocket server setup and client instantiation - Removed the request validation logic from the WebSocket connection handler. - Added a cleanup function to close the WebSocket server. - Introduced a singleton pattern for the WebSocket client to manage connections more efficiently. --- apps/dokploy/server/wss/drawer-logs.ts | 16 ++++++--------- apps/dokploy/utils/api.ts | 27 +++++++++++++++++++------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/apps/dokploy/server/wss/drawer-logs.ts b/apps/dokploy/server/wss/drawer-logs.ts index 404dfeee..dcdeaad7 100644 --- a/apps/dokploy/server/wss/drawer-logs.ts +++ b/apps/dokploy/server/wss/drawer-logs.ts @@ -1,5 +1,4 @@ import type http from "node:http"; -import { validateRequest } from "@dokploy/server/index"; import { applyWSSHandler } from "@trpc/server/adapters/ws"; import { WebSocketServer } from "ws"; import { appRouter } from "../api/root"; @@ -13,11 +12,13 @@ export const setupDrawerLogsWebSocketServer = ( path: "/drawer-logs", }); + // Set up tRPC WebSocket handler applyWSSHandler({ wss: wssTerm, router: appRouter, createContext: createTRPCContext as any, }); + server.on("upgrade", (req, socket, head) => { const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); @@ -31,13 +32,8 @@ export const setupDrawerLogsWebSocketServer = ( } }); - wssTerm.on("connection", async (ws, req) => { - const _url = new URL(req.url || "", `http://${req.headers.host}`); - const { user, session } = await validateRequest(req); - - if (!user || !session) { - ws.close(); - return; - } - }); + // Return cleanup function + return () => { + wssTerm.close(); + }; }; diff --git a/apps/dokploy/utils/api.ts b/apps/dokploy/utils/api.ts index 56197528..7c003f48 100644 --- a/apps/dokploy/utils/api.ts +++ b/apps/dokploy/utils/api.ts @@ -27,15 +27,28 @@ const getWsUrl = () => { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const host = window.location.host; + // Use the base URL for all tRPC WebSocket connections return `${protocol}${host}/drawer-logs`; }; -const wsClient = - typeof window !== "undefined" - ? createWSClient({ - url: getWsUrl() || "", - }) - : null; +// Singleton WebSocket client instance +let wsClientInstance: ReturnType | null = null; + +const getWsClient = () => { + if (typeof window === "undefined") return null; + + if (!wsClientInstance) { + wsClientInstance = createWSClient({ + url: getWsUrl() || "", + onClose: () => { + // Reset the instance when connection closes so it can be recreated + wsClientInstance = null; + }, + }); + } + + return wsClientInstance; +}; /** A set of type-safe react-query hooks for your tRPC API. */ export const api = createTRPCNext({ @@ -57,7 +70,7 @@ export const api = createTRPCNext({ splitLink({ condition: (op) => op.type === "subscription", true: wsLink({ - client: wsClient!, + client: getWsClient()!, }), false: splitLink({ condition: (op) => op.input instanceof FormData, From eff2657e70fc76429e15a67e3f433d3c24d27442 Mon Sep 17 00:00:00 2001 From: krokodaws <46326151+krokodaws@users.noreply.github.com> Date: Fri, 4 Apr 2025 19:21:30 +0300 Subject: [PATCH 08/19] fix: resolve incorrect endpoints for database bulk actions (#1626) Update bulk action endpoints for database services: - Use `/api/trpc/redis.start` and `/api/trpc/redis.stop` for Redis - Use `/api/trpc/postgres.start` and `/api/trpc/postgres.stop` for PostgreSQL - Retain `/api/trpc/compose.start` and `/api/trpc/compose.stop` for Docker Compose services Tested with a project including Gitea, Redis, and PostgreSQL. Bulk start/stop operations now function correctly for all service types. Closes #1626 --- .../pages/dashboard/project/[projectId].tsx | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId].tsx b/apps/dokploy/pages/dashboard/project/[projectId].tsx index e3cfce16..6c4ac4bc 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId].tsx @@ -314,31 +314,43 @@ const Project = ( }; const applicationActions = { + start: api.application.start.useMutation(), + stop: api.application.stop.useMutation(), move: api.application.move.useMutation(), delete: api.application.delete.useMutation(), }; const postgresActions = { + start: api.postgres.start.useMutation(), + stop: api.postgres.stop.useMutation(), move: api.postgres.move.useMutation(), delete: api.postgres.remove.useMutation(), }; const mysqlActions = { + start: api.mysql.start.useMutation(), + stop: api.mysql.stop.useMutation(), move: api.mysql.move.useMutation(), delete: api.mysql.remove.useMutation(), }; const mariadbActions = { + start: api.mariadb.start.useMutation(), + stop: api.mariadb.stop.useMutation(), move: api.mariadb.move.useMutation(), delete: api.mariadb.remove.useMutation(), }; const redisActions = { + start: api.redis.start.useMutation(), + stop: api.redis.stop.useMutation(), move: api.redis.move.useMutation(), delete: api.redis.remove.useMutation(), }; const mongoActions = { + start: api.mongo.start.useMutation(), + stop: api.mongo.stop.useMutation(), move: api.mongo.move.useMutation(), delete: api.mongo.remove.useMutation(), }; @@ -348,7 +360,32 @@ const Project = ( setIsBulkActionLoading(true); for (const serviceId of selectedServices) { try { - await composeActions.start.mutateAsync({ composeId: serviceId }); + const service = filteredServices.find((s) => s.id === serviceId); + if (!service) continue; + + switch (service.type) { + case "application": + await applicationActions.start.mutateAsync({ applicationId: serviceId }); + break; + case "compose": + await composeActions.start.mutateAsync({ composeId: serviceId }); + break; + case "postgres": + await postgresActions.start.mutateAsync({ postgresId: serviceId }); + break; + case "mysql": + await mysqlActions.start.mutateAsync({ mysqlId: serviceId }); + break; + case "mariadb": + await mariadbActions.start.mutateAsync({ mariadbId: serviceId }); + break; + case "redis": + await redisActions.start.mutateAsync({ redisId: serviceId }); + break; + case "mongo": + await mongoActions.start.mutateAsync({ mongoId: serviceId }); + break; + } success++; } catch (_error) { toast.error(`Error starting service ${serviceId}`); @@ -368,7 +405,32 @@ const Project = ( setIsBulkActionLoading(true); for (const serviceId of selectedServices) { try { - await composeActions.stop.mutateAsync({ composeId: serviceId }); + const service = filteredServices.find((s) => s.id === serviceId); + if (!service) continue; + + switch (service.type) { + case "application": + await applicationActions.stop.mutateAsync({ applicationId: serviceId }); + break; + case "compose": + await composeActions.stop.mutateAsync({ composeId: serviceId }); + break; + case "postgres": + await postgresActions.stop.mutateAsync({ postgresId: serviceId }); + break; + case "mysql": + await mysqlActions.stop.mutateAsync({ mysqlId: serviceId }); + break; + case "mariadb": + await mariadbActions.stop.mutateAsync({ mariadbId: serviceId }); + break; + case "redis": + await redisActions.stop.mutateAsync({ redisId: serviceId }); + break; + case "mongo": + await mongoActions.stop.mutateAsync({ mongoId: serviceId }); + break; + } success++; } catch (_error) { toast.error(`Error stopping service ${serviceId}`); From 2c09b63bf9a25c38b0206acfb8d761dfa6f08eea Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Fri, 4 Apr 2025 19:19:09 +0200 Subject: [PATCH 09/19] feat: improve projects show grid --- apps/dokploy/components/dashboard/projects/show.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 31ba80c8..03ebe7a8 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -115,7 +115,7 @@ export const ShowProjects = () => {
)} -
+
{filteredProjects?.map((project) => { const emptyServices = project?.mariadb.length === 0 && From 5863e45c13916845c1b7581faae44c6c1a0b2a1c Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Fri, 4 Apr 2025 20:18:56 +0200 Subject: [PATCH 10/19] remove sensitive files on static build --- packages/server/src/utils/builders/static.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/server/src/utils/builders/static.ts b/packages/server/src/utils/builders/static.ts index c46bdf2e..f7fc87ca 100644 --- a/packages/server/src/utils/builders/static.ts +++ b/packages/server/src/utils/builders/static.ts @@ -25,6 +25,12 @@ export const buildStatic = async ( ].join("\n"), ); + createFile( + buildAppDirectory, + ".dockerignore", + [".git", ".env", "Dockerfile", ".dockerignore"].join("\n"), + ); + await buildCustomDocker( { ...application, From cb20950dd9222dba19e3f48404416e4414f7eabb Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 5 Apr 2025 23:03:57 -0600 Subject: [PATCH 11/19] feat(registry): refactor Docker login command execution to use execFileAsync for improved input handling --- apps/dokploy/server/api/routers/registry.ts | 19 +++++++-- .../server/src/utils/process/execAsync.ts | 41 ++++++++++++++++++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/server/api/routers/registry.ts b/apps/dokploy/server/api/routers/registry.ts index a9a6be89..5486f37c 100644 --- a/apps/dokploy/server/api/routers/registry.ts +++ b/apps/dokploy/server/api/routers/registry.ts @@ -10,8 +10,8 @@ import { import { IS_CLOUD, createRegistry, - execAsync, execAsyncRemote, + execFileAsync, findRegistryById, removeRegistry, updateRegistry, @@ -83,7 +83,13 @@ export const registryRouter = createTRPCRouter({ .input(apiTestRegistry) .mutation(async ({ input }) => { try { - const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; + const args = [ + "login", + input.registryUrl, + "--username", + input.username, + "--password-stdin", + ]; if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -93,9 +99,14 @@ export const registryRouter = createTRPCRouter({ } if (input.serverId && input.serverId !== "none") { - await execAsyncRemote(input.serverId, loginCommand); + await execAsyncRemote( + input.serverId, + `echo ${input.password} | docker ${args.join(" ")}`, + ); } else { - await execAsync(loginCommand); + await execFileAsync("docker", args, { + input: Buffer.from(input.password).toString(), + }); } return true; diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index aee1e821..c3e40907 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -1,9 +1,48 @@ -import { exec } from "node:child_process"; +import { exec, execFile } from "node:child_process"; import util from "node:util"; import { findServerById } from "@dokploy/server/services/server"; import { Client } from "ssh2"; + export const execAsync = util.promisify(exec); +export const execFileAsync = async ( + command: string, + args: string[], + options: { input?: string } = {}, +): Promise<{ stdout: string; stderr: string }> => { + const child = execFile(command, args); + + if (options.input && child.stdin) { + child.stdin.write(options.input); + child.stdin.end(); + } + + return new Promise((resolve, reject) => { + let stdout = ""; + let stderr = ""; + + child.stdout?.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr?.on("data", (data) => { + stderr += data.toString(); + }); + + child.on("close", (code) => { + if (code === 0) { + resolve({ stdout, stderr }); + } else { + reject( + new Error(`Command failed with code ${code}. Stderr: ${stderr}`), + ); + } + }); + + child.on("error", reject); + }); +}; + export const execAsyncRemote = async ( serverId: string | null, command: string, From 14bc26e065dc572b839c6cbd44b1a6ed26d80613 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 6 Apr 2025 00:07:41 -0600 Subject: [PATCH 12/19] feat(websocket): enhance WebSocket server with request validation and client instantiation - Added request validation to ensure user authentication before establishing WebSocket connections. - Refactored WebSocket client instantiation to simplify connection management. --- apps/dokploy/server/wss/drawer-logs.ts | 14 +++++++++---- apps/dokploy/utils/api.ts | 27 +++++++------------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/apps/dokploy/server/wss/drawer-logs.ts b/apps/dokploy/server/wss/drawer-logs.ts index dcdeaad7..0202ae52 100644 --- a/apps/dokploy/server/wss/drawer-logs.ts +++ b/apps/dokploy/server/wss/drawer-logs.ts @@ -3,6 +3,7 @@ import { applyWSSHandler } from "@trpc/server/adapters/ws"; import { WebSocketServer } from "ws"; import { appRouter } from "../api/root"; import { createTRPCContext } from "../api/trpc"; +import { validateRequest } from "@dokploy/server/lib/auth"; export const setupDrawerLogsWebSocketServer = ( server: http.Server, @@ -32,8 +33,13 @@ export const setupDrawerLogsWebSocketServer = ( } }); - // Return cleanup function - return () => { - wssTerm.close(); - }; + wssTerm.on("connection", async (ws, req) => { + const _url = new URL(req.url || "", `http://${req.headers.host}`); + const { user, session } = await validateRequest(req); + + if (!user || !session) { + ws.close(); + return; + } + }); }; diff --git a/apps/dokploy/utils/api.ts b/apps/dokploy/utils/api.ts index 7c003f48..56197528 100644 --- a/apps/dokploy/utils/api.ts +++ b/apps/dokploy/utils/api.ts @@ -27,28 +27,15 @@ const getWsUrl = () => { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const host = window.location.host; - // Use the base URL for all tRPC WebSocket connections return `${protocol}${host}/drawer-logs`; }; -// Singleton WebSocket client instance -let wsClientInstance: ReturnType | null = null; - -const getWsClient = () => { - if (typeof window === "undefined") return null; - - if (!wsClientInstance) { - wsClientInstance = createWSClient({ - url: getWsUrl() || "", - onClose: () => { - // Reset the instance when connection closes so it can be recreated - wsClientInstance = null; - }, - }); - } - - return wsClientInstance; -}; +const wsClient = + typeof window !== "undefined" + ? createWSClient({ + url: getWsUrl() || "", + }) + : null; /** A set of type-safe react-query hooks for your tRPC API. */ export const api = createTRPCNext({ @@ -70,7 +57,7 @@ export const api = createTRPCNext({ splitLink({ condition: (op) => op.type === "subscription", true: wsLink({ - client: getWsClient()!, + client: wsClient!, }), false: splitLink({ condition: (op) => op.input instanceof FormData, From 1605aedd6e55d6efe299b725e31317e2cbc7a916 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 6 Apr 2025 01:41:47 -0600 Subject: [PATCH 13/19] feat(settings): add HTTPS support and update user schema - Introduced a new boolean field 'https' in the user schema to manage HTTPS settings. - Updated the web domain form to include an HTTPS toggle, allowing users to enable or disable HTTPS. - Enhanced validation logic to ensure certificate type is required when HTTPS is enabled. - Modified Traefik configuration to handle HTTPS routing based on user settings. --- .../dashboard/settings/web-domain.tsx | 111 +- apps/dokploy/drizzle/0084_thin_iron_lad.sql | 1 + apps/dokploy/drizzle/meta/0084_snapshot.json | 5369 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + apps/dokploy/server/api/routers/settings.ts | 1 + packages/server/src/db/schema/user.ts | 3 + .../server/src/utils/traefik/web-server.ts | 56 +- 7 files changed, 5500 insertions(+), 48 deletions(-) create mode 100644 apps/dokploy/drizzle/0084_thin_iron_lad.sql create mode 100644 apps/dokploy/drizzle/meta/0084_snapshot.json diff --git a/apps/dokploy/components/dashboard/settings/web-domain.tsx b/apps/dokploy/components/dashboard/settings/web-domain.tsx index a579df39..d35dae35 100644 --- a/apps/dokploy/components/dashboard/settings/web-domain.tsx +++ b/apps/dokploy/components/dashboard/settings/web-domain.tsx @@ -9,6 +9,7 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -22,6 +23,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { GlobeIcon } from "lucide-react"; @@ -33,11 +35,19 @@ import { z } from "zod"; const addServerDomain = z .object({ - domain: z.string().min(1, { message: "URL is required" }), + domain: z.string(), letsEncryptEmail: z.string(), + https: z.boolean().optional(), certificateType: z.enum(["letsencrypt", "none", "custom"]), }) .superRefine((data, ctx) => { + if (data.https && !data.certificateType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["certificateType"], + message: "Required", + }); + } if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -61,15 +71,18 @@ export const WebDomain = () => { domain: "", certificateType: "none", letsEncryptEmail: "", + https: false, }, resolver: zodResolver(addServerDomain), }); + const https = form.watch("https"); useEffect(() => { if (data) { form.reset({ domain: data?.user?.host || "", certificateType: data?.user?.certificateType, letsEncryptEmail: data?.user?.letsEncryptEmail || "", + https: data?.user?.https || false, }); } }, [form, form.reset, data]); @@ -79,6 +92,7 @@ export const WebDomain = () => { host: data.domain, letsEncryptEmail: data.letsEncryptEmail, certificateType: data.certificateType, + https: data.https, }) .then(async () => { await refetch(); @@ -155,44 +169,67 @@ export const WebDomain = () => { /> { - return ( - - - {t("settings.server.domain.form.certificate.label")} - - + name="https" + render={({ field }) => ( + +
+ HTTPS + + Automatically provision SSL Certificate. + - - ); - }} +
+ + + +
+ )} /> + {https && ( + { + return ( + + + {t("settings.server.domain.form.certificate.label")} + + + + + ); + }} + /> + )}
- - - - -
- -
- ); -}; - -export default Page; - -Page.getLayout = (page: ReactElement) => { - return {page}; -}; -export async function getServerSideProps( - ctx: GetServerSidePropsContext<{ serviceId: string }>, -) { - const { req, res } = ctx; - const { user, session } = await validateRequest(ctx.req); - if (!user) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - if (user.role === "member") { - return { - redirect: { - permanent: true, - destination: "/dashboard/settings/profile", - }, - }; - } - - const helpers = createServerSideHelpers({ - router: appRouter, - ctx: { - req: req as any, - res: res as any, - db: null as any, - session: session as any, - user: user as any, - }, - transformer: superjson, - }); - await helpers.user.get.prefetch(); - - return { - props: { - trpcState: helpers.dehydrate(), - }, - }; -}