From 0393c2635bf0a4314792d6bd19901b07e81e2977 Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Wed, 8 Jul 2026 10:17:05 +0530 Subject: [PATCH 01/31] feat: add drag-and-drop logo upload for organizations - Integrated Dropzone component into handle-organization.tsx. - Resolved dialog auto-close bug by decoupling Dialog from DropdownMenu trigger. - Added DOMPurify to sanitize uploaded SVG logos preventing XSS. - Implemented client-side compression via canvas to resize raster images to 256x256 WebP. - Enhanced UX with instant logo preview, masked base64 input strings, and a quick-clear button. - Updated Dropzone to accept classNameContent for local height overrides. --- .../organization/handle-organization.tsx | 232 +++++++++++++++--- apps/dokploy/components/layouts/side.tsx | 46 +++- apps/dokploy/components/ui/dropzone.tsx | 8 +- 3 files changed, 248 insertions(+), 38 deletions(-) diff --git a/apps/dokploy/components/dashboard/organization/handle-organization.tsx b/apps/dokploy/components/dashboard/organization/handle-organization.tsx index 1a3bd919d..a2a61047e 100644 --- a/apps/dokploy/components/dashboard/organization/handle-organization.tsx +++ b/apps/dokploy/components/dashboard/organization/handle-organization.tsx @@ -1,5 +1,6 @@ import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; -import { PenBoxIcon, Plus } from "lucide-react"; +import DOMPurify from "dompurify"; +import { GlobeIcon, PenBoxIcon, Plus, X } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -15,6 +16,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { Dropzone } from "@/components/ui/dropzone"; import { Form, FormControl, @@ -38,10 +40,63 @@ type OrganizationFormValues = z.infer; interface Props { organizationId?: string; children?: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; } -export function AddOrganization({ organizationId }: Props) { - const [open, setOpen] = useState(false); +const sanitizeSvg = (svgContent: string): string | null => { + const clean = DOMPurify.sanitize(svgContent, { + USE_PROFILES: { svg: true, svgFilters: true }, + ADD_TAGS: ["use"], + }); + if (!clean) return null; + return `data:image/svg+xml;base64,${btoa(clean)}`; +}; + +const resizeImage = (file: File, maxSize: number): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (event) => { + const img = new Image(); + img.onload = () => { + let { width, height } = img; + + if (width > maxSize || height > maxSize) { + if (width > height) { + height = Math.round((height * maxSize) / width); + width = maxSize; + } else { + width = Math.round((width * maxSize) / height); + height = maxSize; + } + } + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + if (!ctx) { + resolve(event.target?.result as string); + return; + } + + ctx.drawImage(img, 0, 0, width, height); + resolve(canvas.toDataURL("image/webp", 0.8)); + }; + img.onerror = reject; + img.src = event.target?.result as string; + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); +}; + +export function AddOrganization({ organizationId, open: controlledOpen, onOpenChange: controlledOnOpenChange }: Props) { + const [internalOpen, setInternalOpen] = useState(false); + const [uploadedFileName, setUploadedFileName] = useState(null); + const isControlled = controlledOpen !== undefined; + const open = isControlled ? controlledOpen : internalOpen; + const setOpen = isControlled ? controlledOnOpenChange! : setInternalOpen; const utils = api.useUtils(); const { data: organization } = api.organization.one.useQuery( { @@ -69,6 +124,7 @@ export function AddOrganization({ organizationId }: Props) { name: organization.name, logo: organization.logo || "", }); + setUploadedFileName(null); } }, [organization, form]); @@ -80,6 +136,7 @@ export function AddOrganization({ organizationId }: Props) { }) .then(() => { form.reset(); + setUploadedFileName(null); toast.success( `Organization ${organizationId ? "updated" : "created"} successfully`, ); @@ -99,30 +156,86 @@ export function AddOrganization({ organizationId }: Props) { }); }; + const handleFileUpload = async (files: FileList | null) => { + if (!files || files.length === 0) return; + const file = files[0]; + if (!file) return; + + const allowedTypes = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/svg+xml", + "image/webp", + ]; + const fileExtension = file.name.split(".").pop()?.toLowerCase(); + const allowedExtensions = ["jpg", "jpeg", "png", "svg", "webp"]; + + if ( + !allowedTypes.includes(file.type) && + !allowedExtensions.includes(fileExtension || "") + ) { + toast.error("Only JPG, JPEG, PNG, WEBP, and SVG files are allowed"); + return; + } + + if (file.size > 2 * 1024 * 1024) { + toast.error("Image size must be less than 2MB"); + return; + } + + const isSvg = file.type === "image/svg+xml" || fileExtension === "svg"; + + if (isSvg) { + const text = await file.text(); + const sanitizedDataUrl = sanitizeSvg(text); + if (!sanitizedDataUrl) { + toast.error("Invalid SVG file"); + return; + } + form.setValue("logo", sanitizedDataUrl); + form.trigger("logo"); + setUploadedFileName(file.name); + return; + } + + // Resize raster images to max 256x256 and convert to WebP to save space + try { + const resizedDataUrl = await resizeImage(file, 256); + form.setValue("logo", resizedDataUrl); + form.trigger("logo"); + setUploadedFileName(file.name); + } catch (error) { + toast.error("Error processing image"); + } + }; + return ( - - {organizationId ? ( - e.preventDefault()} - > - - - ) : ( - e.preventDefault()} - > -
- -
-
- Add organization -
-
- )} -
+ {!isControlled && ( + + {organizationId ? ( + e.preventDefault()} + > + + + ) : ( + e.preventDefault()} + > +
+ +
+
+ Add organization +
+
+ )} +
+ )} @@ -159,20 +272,71 @@ export function AddOrganization({ organizationId }: Props) { ( + render={({ field }) => { + const isDataUrl = field.value?.startsWith("data:"); + const displayValue = isDataUrl + ? uploadedFileName || "Uploaded image" + : field.value || ""; + + return ( - Logo URL + + Logo URL or Upload + - +
+
+
+ {field.value ? ( + // biome-ignore lint/performance/noImgElement: user uploaded logo preview + Logo preview + ) : ( + + )} +
+
+ { + field.onChange(e); + if (isDataUrl) setUploadedFileName(null); + }} + className="w-full pr-8" + /> + {field.value && ( + + )} +
+
+ +
- )} + ); + }} /> {org.ownerId === session?.user?.id && ( <> - + { + e.preventDefault(); + setEditOrganizationId(org.id); + }} + > + + - + { + e.preventDefault(); + setIsAddOrganizationOpen(true); + }} + > +
+ +
+
+ Add organization +
+
)} @@ -870,6 +895,23 @@ function SidebarLogo() { )} + {editOrganizationId && ( + { + if (!open) setEditOrganizationId(null); + }} + /> + )} + {isAddOrganizationOpen && ( + { + if (!open) setIsAddOrganizationOpen(false); + }} + /> + )} ); } diff --git a/apps/dokploy/components/ui/dropzone.tsx b/apps/dokploy/components/ui/dropzone.tsx index 6d9c05ef6..460935078 100644 --- a/apps/dokploy/components/ui/dropzone.tsx +++ b/apps/dokploy/components/ui/dropzone.tsx @@ -10,13 +10,14 @@ interface DropzoneProps "value" | "onChange" > { classNameWrapper?: string; + classNameContent?: string; className?: string; dropMessage: string; onChange: (acceptedFiles: FileList | null) => void; } export const Dropzone = React.forwardRef( - ({ className, classNameWrapper, dropMessage, onChange, ...props }, ref) => { + ({ className, classNameWrapper, classNameContent, dropMessage, onChange, ...props }, ref) => { const inputRef = useRef(null); // Function to handle drag over event const handleDragOver = (e: React.DragEvent) => { @@ -51,7 +52,10 @@ export const Dropzone = React.forwardRef( )} > Date: Wed, 8 Jul 2026 05:00:40 +0000 Subject: [PATCH 02/31] [autofix.ci] apply automated fixes --- .../organization/handle-organization.tsx | 114 +++++++++--------- apps/dokploy/components/layouts/side.tsx | 4 +- apps/dokploy/components/ui/command.tsx | 4 +- apps/dokploy/components/ui/dropzone.tsx | 14 ++- 4 files changed, 75 insertions(+), 61 deletions(-) diff --git a/apps/dokploy/components/dashboard/organization/handle-organization.tsx b/apps/dokploy/components/dashboard/organization/handle-organization.tsx index a2a61047e..4868ce37f 100644 --- a/apps/dokploy/components/dashboard/organization/handle-organization.tsx +++ b/apps/dokploy/components/dashboard/organization/handle-organization.tsx @@ -91,7 +91,11 @@ const resizeImage = (file: File, maxSize: number): Promise => { }); }; -export function AddOrganization({ organizationId, open: controlledOpen, onOpenChange: controlledOnOpenChange }: Props) { +export function AddOrganization({ + organizationId, + open: controlledOpen, + onOpenChange: controlledOnOpenChange, +}: Props) { const [internalOpen, setInternalOpen] = useState(false); const [uploadedFileName, setUploadedFileName] = useState(null); const isControlled = controlledOpen !== undefined; @@ -277,64 +281,64 @@ export function AddOrganization({ organizationId, open: controlledOpen, onOpenCh const displayValue = isDataUrl ? uploadedFileName || "Uploaded image" : field.value || ""; - + return ( - - - Logo URL or Upload - - -
-
-
- {field.value ? ( - // biome-ignore lint/performance/noImgElement: user uploaded logo preview - Logo preview - ) : ( - - )} -
-
- { - field.onChange(e); - if (isDataUrl) setUploadedFileName(null); - }} - className="w-full pr-8" - /> - {field.value && ( - - )} + className="w-full pr-8" + /> + {field.value && ( + + )} +
+
- - -
- -
+ + + ); }} /> diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 9a7a972e3..1e78604d8 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -576,7 +576,9 @@ function SidebarLogo() { const [_activeTeam, setActiveTeam] = useState< typeof activeOrganization | null >(null); - const [editOrganizationId, setEditOrganizationId] = useState(null); + const [editOrganizationId, setEditOrganizationId] = useState( + null, + ); const [isAddOrganizationOpen, setIsAddOrganizationOpen] = useState(false); useEffect(() => { diff --git a/apps/dokploy/components/ui/command.tsx b/apps/dokploy/components/ui/command.tsx index 6e5b0921d..99866b79f 100644 --- a/apps/dokploy/components/ui/command.tsx +++ b/apps/dokploy/components/ui/command.tsx @@ -56,9 +56,7 @@ function CommandDialog({ {title} {description}
- - {children} - + {children}
); diff --git a/apps/dokploy/components/ui/dropzone.tsx b/apps/dokploy/components/ui/dropzone.tsx index 460935078..5ae7c9d3e 100644 --- a/apps/dokploy/components/ui/dropzone.tsx +++ b/apps/dokploy/components/ui/dropzone.tsx @@ -17,7 +17,17 @@ interface DropzoneProps } export const Dropzone = React.forwardRef( - ({ className, classNameWrapper, classNameContent, dropMessage, onChange, ...props }, ref) => { + ( + { + className, + classNameWrapper, + classNameContent, + dropMessage, + onChange, + ...props + }, + ref, + ) => { const inputRef = useRef(null); // Function to handle drag over event const handleDragOver = (e: React.DragEvent) => { @@ -54,7 +64,7 @@ export const Dropzone = React.forwardRef( Date: Wed, 8 Jul 2026 23:59:02 +0530 Subject: [PATCH 03/31] fix: eliminate FOUC for whitelabeling across all pages --- .../whitelabeling/whitelabeling-provider.tsx | 31 ---- apps/dokploy/pages/_app.tsx | 6 - apps/dokploy/pages/_document.tsx | 148 +++++++++++++++++- apps/dokploy/pages/index.tsx | 21 ++- apps/dokploy/pages/invitation.tsx | 20 +++ apps/dokploy/pages/register.tsx | 20 +++ apps/dokploy/pages/send-reset-password.tsx | 24 ++- .../api/routers/proprietary/whitelabeling.ts | 14 ++ 8 files changed, 241 insertions(+), 43 deletions(-) delete mode 100644 apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx diff --git a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx deleted file mode 100644 index a1a327561..000000000 --- a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import Head from "next/head"; -import { api } from "@/utils/api"; - -export function WhitelabelingProvider() { - const { data: config } = api.whitelabeling.getPublic.useQuery(undefined, { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - }); - - if (!config) return null; - - return ( - <> - - {config.metaTitle && {config.metaTitle}} - {config.faviconUrl && } - - - {config.customCss && ( - - - Dokploy - - {getLayout()} diff --git a/apps/dokploy/pages/_document.tsx b/apps/dokploy/pages/_document.tsx index 120bb827e..bfe175c80 100644 --- a/apps/dokploy/pages/_document.tsx +++ b/apps/dokploy/pages/_document.tsx @@ -1,10 +1,104 @@ -import { Head, Html, Main, NextScript } from "next/document"; +import { getPublicWhitelabelingConfig } from "@dokploy/server"; +import NextDocument, { + type DocumentContext, + type DocumentInitialProps, + Head, + Html, + Main, + NextScript, +} from "next/document"; -export default function Document() { +interface WhitelabelingDocumentProps { + metaTitle: string | null; + faviconHref: string | null; + customCss: string | null; +} + +// Cache the resolved favicon (inlined as a data URI) so we don't re-fetch the +// remote image on every server render. Keyed by the configured favicon URL. +const FAVICON_CACHE_TTL = 60 * 60 * 1000; // 1 hour + +const SETTINGS_CACHE_TTL = 60 * 1000; // 1 minute + +declare global { + var __SETTINGS_CACHE: { + data: { + metaTitle: string | null; + faviconHref: string | null; + customCss: string | null; + }; + expiresAt: number; + } | null; + var __FAVICON_CACHE: Map; +} + +const faviconCache = + globalThis.__FAVICON_CACHE || + new Map(); +globalThis.__FAVICON_CACHE = faviconCache; + +/** + * Resolve the favicon to an inline data URI so it is present in the initial + * HTML and renders without a network round-trip (no flash of the default + * favicon). Falls back to the raw URL if the image can't be fetched. + */ +async function resolveFaviconHref( + faviconUrl: string | null | undefined, +): Promise { + if (!faviconUrl) return null; + + const cached = faviconCache.get(faviconUrl); + if (cached && cached.expiresAt > Date.now()) { + return cached.href; + } + + // Default to the raw URL so the custom favicon still loads if inlining fails. + let href = faviconUrl; + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 3000); + const response = await fetch(faviconUrl, { signal: controller.signal }); + clearTimeout(timeout); + + if (response.ok) { + const buffer = Buffer.from(await response.arrayBuffer()); + // Avoid embedding very large images directly in the HTML. + if (buffer.byteLength <= 512 * 1024) { + const contentType = response.headers.get("content-type") || "image/png"; + href = `data:${contentType};base64,${buffer.toString("base64")}`; + } + } + } catch { + // Keep the raw URL fallback. + } + + faviconCache.set(faviconUrl, { + href, + expiresAt: Date.now() + FAVICON_CACHE_TTL, + }); + return href; +} + +export default function Document({ + metaTitle, + faviconHref, + customCss, +}: WhitelabelingDocumentProps) { + const title = metaTitle || "Dokploy"; return ( - + {/* Rendered on the server so the correct branding is present on first + paint (and for social scrapers), avoiding a flash of / fallback to + the default Dokploy branding. */} + {title} + + {customCss && ( + tags. - Remove SSR favicon fetching to prevent SSRF and internal network scanning, falling back to client-side fetching. --- apps/dokploy/pages/_document.tsx | 81 ++++--------------- .../api/routers/proprietary/whitelabeling.ts | 3 - 2 files changed, 16 insertions(+), 68 deletions(-) diff --git a/apps/dokploy/pages/_document.tsx b/apps/dokploy/pages/_document.tsx index bfe175c80..2046b6f9f 100644 --- a/apps/dokploy/pages/_document.tsx +++ b/apps/dokploy/pages/_document.tsx @@ -14,70 +14,7 @@ interface WhitelabelingDocumentProps { customCss: string | null; } -// Cache the resolved favicon (inlined as a data URI) so we don't re-fetch the -// remote image on every server render. Keyed by the configured favicon URL. -const FAVICON_CACHE_TTL = 60 * 60 * 1000; // 1 hour -const SETTINGS_CACHE_TTL = 60 * 1000; // 1 minute - -declare global { - var __SETTINGS_CACHE: { - data: { - metaTitle: string | null; - faviconHref: string | null; - customCss: string | null; - }; - expiresAt: number; - } | null; - var __FAVICON_CACHE: Map; -} - -const faviconCache = - globalThis.__FAVICON_CACHE || - new Map(); -globalThis.__FAVICON_CACHE = faviconCache; - -/** - * Resolve the favicon to an inline data URI so it is present in the initial - * HTML and renders without a network round-trip (no flash of the default - * favicon). Falls back to the raw URL if the image can't be fetched. - */ -async function resolveFaviconHref( - faviconUrl: string | null | undefined, -): Promise { - if (!faviconUrl) return null; - - const cached = faviconCache.get(faviconUrl); - if (cached && cached.expiresAt > Date.now()) { - return cached.href; - } - - // Default to the raw URL so the custom favicon still loads if inlining fails. - let href = faviconUrl; - try { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 3000); - const response = await fetch(faviconUrl, { signal: controller.signal }); - clearTimeout(timeout); - - if (response.ok) { - const buffer = Buffer.from(await response.arrayBuffer()); - // Avoid embedding very large images directly in the HTML. - if (buffer.byteLength <= 512 * 1024) { - const contentType = response.headers.get("content-type") || "image/png"; - href = `data:${contentType};base64,${buffer.toString("base64")}`; - } - } - } catch { - // Keep the raw URL fallback. - } - - faviconCache.set(faviconUrl, { - href, - expiresAt: Date.now() + FAVICON_CACHE_TTL, - }); - return href; -} export default function Document({ metaTitle, @@ -108,6 +45,19 @@ export default function Document({ ); } +const SETTINGS_CACHE_TTL = 60 * 1000; // 1 minute + +declare global { + var __SETTINGS_CACHE: { + data: { + metaTitle: string | null; + faviconHref: string | null; + customCss: string | null; + }; + expiresAt: number; + } | null; +} + Document.getInitialProps = async ( ctx: DocumentContext, ): Promise => { @@ -132,8 +82,9 @@ Document.getInitialProps = async ( const config = await getPublicWhitelabelingConfig(); if (config) { metaTitle = config.metaTitle; - customCss = config.customCss; - faviconHref = await resolveFaviconHref(config.faviconUrl); + // Remove any tags to prevent XSS breakout + customCss = config.customCss ? config.customCss.replace(/<\/style>/gi, "") : null; + faviconHref = config.faviconUrl || null; } } catch { // Fall back to defaults if settings can't be read (e.g. DB not ready) diff --git a/apps/dokploy/server/api/routers/proprietary/whitelabeling.ts b/apps/dokploy/server/api/routers/proprietary/whitelabeling.ts index 6bc0d0f8f..bc38c8ff1 100644 --- a/apps/dokploy/server/api/routers/proprietary/whitelabeling.ts +++ b/apps/dokploy/server/api/routers/proprietary/whitelabeling.ts @@ -17,9 +17,6 @@ import { /** Invalidate the SSR branding caches in _document.tsx so the next request picks up fresh settings. */ function clearBrandingSSRCache() { globalThis.__SETTINGS_CACHE = null; - if (globalThis.__FAVICON_CACHE) { - globalThis.__FAVICON_CACHE.clear(); - } } export const whitelabelingRouter = createTRPCRouter({ From 58f3714ddc5ad256bb039801623691d9c83b7d78 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:01:38 +0000 Subject: [PATCH 12/31] [autofix.ci] apply automated fixes --- apps/dokploy/pages/_document.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/pages/_document.tsx b/apps/dokploy/pages/_document.tsx index 2046b6f9f..127a153d1 100644 --- a/apps/dokploy/pages/_document.tsx +++ b/apps/dokploy/pages/_document.tsx @@ -14,8 +14,6 @@ interface WhitelabelingDocumentProps { customCss: string | null; } - - export default function Document({ metaTitle, faviconHref, @@ -83,7 +81,9 @@ Document.getInitialProps = async ( if (config) { metaTitle = config.metaTitle; // Remove any tags to prevent XSS breakout - customCss = config.customCss ? config.customCss.replace(/<\/style>/gi, "") : null; + customCss = config.customCss + ? config.customCss.replace(/<\/style>/gi, "") + : null; faviconHref = config.faviconUrl || null; } } catch { From 5606a2bd1ebe1278cadae974aa70e70460fd1ace Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Wed, 2 Sep 2026 01:44:46 +0530 Subject: [PATCH 13/31] fix: account for whitespace in style tag regex to prevent XSS --- apps/dokploy/pages/_document.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/pages/_document.tsx b/apps/dokploy/pages/_document.tsx index 127a153d1..b50fd6f12 100644 --- a/apps/dokploy/pages/_document.tsx +++ b/apps/dokploy/pages/_document.tsx @@ -82,7 +82,7 @@ Document.getInitialProps = async ( metaTitle = config.metaTitle; // Remove any tags to prevent XSS breakout customCss = config.customCss - ? config.customCss.replace(/<\/style>/gi, "") + ? config.customCss.replace(/<\/\s*style\s*>/gi, "") : null; faviconHref = config.faviconUrl || null; } From 13bb1ac5d155775ad30d591955d96935801f864b Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Wed, 2 Sep 2026 01:49:35 +0530 Subject: [PATCH 14/31] fix: account for all style end-tag variants like --- apps/dokploy/pages/_document.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/pages/_document.tsx b/apps/dokploy/pages/_document.tsx index b50fd6f12..88e2af9a4 100644 --- a/apps/dokploy/pages/_document.tsx +++ b/apps/dokploy/pages/_document.tsx @@ -82,7 +82,7 @@ Document.getInitialProps = async ( metaTitle = config.metaTitle; // Remove any tags to prevent XSS breakout customCss = config.customCss - ? config.customCss.replace(/<\/\s*style\s*>/gi, "") + ? config.customCss.replace(/<\/\s*style[^>]*>/gi, "") : null; faviconHref = config.faviconUrl || null; } From 6d51c100ae76469ef464c7b39e0bd1bb2d2725ee Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:02:55 +0000 Subject: [PATCH 15/31] [autofix.ci] apply automated fixes --- apps/dokploy/components/layouts/side.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index cd38dc53e..66504d7e2 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -29,7 +29,6 @@ import { type LucideIcon, Package, Palette, - Server, ShieldCheck, Smartphone, @@ -755,7 +754,6 @@ function SidebarLogo() { : "Set as default" } > - {isDefault ? ( Date: Wed, 2 Sep 2026 01:55:12 +0530 Subject: [PATCH 16/31] fix: resolve SVG unicode base64 encoding error and image upload race conditions --- .../organization/handle-organization.tsx | 69 +++++++++++++++---- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/apps/dokploy/components/dashboard/organization/handle-organization.tsx b/apps/dokploy/components/dashboard/organization/handle-organization.tsx index bc2e015d0..81fc12534 100644 --- a/apps/dokploy/components/dashboard/organization/handle-organization.tsx +++ b/apps/dokploy/components/dashboard/organization/handle-organization.tsx @@ -1,7 +1,7 @@ import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import DOMPurify from "dompurify"; import { GlobeIcon, PenBoxIcon, Plus, X } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; @@ -49,7 +49,12 @@ const sanitizeSvg = (svgContent: string): string | null => { ADD_TAGS: ["use"], }); if (!clean) return null; - return `data:image/svg+xml;base64,${btoa(clean)}`; + const bytes = new TextEncoder().encode(clean); + let binString = ""; + for (let i = 0; i < bytes.length; i++) { + binString += String.fromCharCode(bytes[i]!); + } + return `data:image/svg+xml;base64,${btoa(binString)}`; }; const resizeImage = (file: File, maxSize: number): Promise => { @@ -97,6 +102,8 @@ export function AddOrganization({ }: Props) { const [internalOpen, setInternalOpen] = useState(false); const [uploadedFileName, setUploadedFileName] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const uploadCounter = useRef(0); const isControlled = controlledOpen !== undefined; const open = isControlled ? controlledOpen : internalOpen; const setOpen = isControlled ? controlledOnOpenChange! : setInternalOpen; @@ -124,6 +131,8 @@ export function AddOrganization({ useEffect(() => { if (organization) { + uploadCounter.current++; + setIsUploading(false); form.reset({ name: organization.name, logo: organization.logo || "", @@ -133,6 +142,7 @@ export function AddOrganization({ }, [organization, form]); const onSubmit = async (values: OrganizationFormValues) => { + if (isUploading) return; await mutateAsync({ name: values.name, logo: values.logo, @@ -165,6 +175,9 @@ export function AddOrganization({ const file = files[0]; if (!file) return; + const currentUploadId = ++uploadCounter.current; + setIsUploading(true); + const allowedTypes = [ "image/jpeg", "image/jpg", @@ -180,42 +193,68 @@ export function AddOrganization({ !allowedExtensions.includes(fileExtension || "") ) { toast.error("Only JPG, JPEG, PNG, WEBP, and SVG files are allowed"); + setIsUploading(false); return; } if (file.size > 2 * 1024 * 1024) { toast.error("Image size must be less than 2MB"); + setIsUploading(false); return; } const isSvg = file.type === "image/svg+xml" || fileExtension === "svg"; if (isSvg) { - const text = await file.text(); - const sanitizedDataUrl = sanitizeSvg(text); - if (!sanitizedDataUrl) { - toast.error("Invalid SVG file"); - return; + try { + const text = await file.text(); + const sanitizedDataUrl = sanitizeSvg(text); + if (currentUploadId !== uploadCounter.current) return; + if (!sanitizedDataUrl) { + toast.error("Invalid SVG file"); + return; + } + form.setValue("logo", sanitizedDataUrl); + form.trigger("logo"); + setUploadedFileName(file.name); + } catch (error) { + if (currentUploadId === uploadCounter.current) { + toast.error("Error processing SVG"); + } + } finally { + if (currentUploadId === uploadCounter.current) { + setIsUploading(false); + } } - form.setValue("logo", sanitizedDataUrl); - form.trigger("logo"); - setUploadedFileName(file.name); return; } // Resize raster images to max 256x256 and convert to WebP to save space try { const resizedDataUrl = await resizeImage(file, 256); + if (currentUploadId !== uploadCounter.current) return; form.setValue("logo", resizedDataUrl); form.trigger("logo"); setUploadedFileName(file.name); } catch (error) { - toast.error("Error processing image"); + if (currentUploadId === uploadCounter.current) { + toast.error("Error processing image"); + } + } finally { + if (currentUploadId === uploadCounter.current) { + setIsUploading(false); + } } }; return ( - + { + if (!val) { + uploadCounter.current++; + setIsUploading(false); + } + setOpen(val); + }}> {organizationId ? ( From 3893150f5cd1685d7271c90915cb22ad90da5f76 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:57:36 +0000 Subject: [PATCH 17/31] [autofix.ci] apply automated fixes --- .../organization/handle-organization.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/dokploy/components/dashboard/organization/handle-organization.tsx b/apps/dokploy/components/dashboard/organization/handle-organization.tsx index 81fc12534..5c7b20194 100644 --- a/apps/dokploy/components/dashboard/organization/handle-organization.tsx +++ b/apps/dokploy/components/dashboard/organization/handle-organization.tsx @@ -248,13 +248,16 @@ export function AddOrganization({ }; return ( - { - if (!val) { - uploadCounter.current++; - setIsUploading(false); - } - setOpen(val); - }}> + { + if (!val) { + uploadCounter.current++; + setIsUploading(false); + } + setOpen(val); + }} + > {organizationId ? (