From b84bc9b7c6a4603b7f3364fb357e2da29b890ad9 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 10 Mar 2026 00:27:58 -0600 Subject: [PATCH 1/7] feat: implement whitelabeling features including settings, preview, and provider components --- .../server/update-server-config.test.ts | 15 + .../impersonation/impersonation-bar.tsx | 7 +- .../components/layouts/onboarding-layout.tsx | 20 +- apps/dokploy/components/layouts/side.tsx | 76 +- .../whitelabeling/whitelabeling-preview.tsx | 85 + .../whitelabeling/whitelabeling-provider.tsx | 93 + .../whitelabeling/whitelabeling-settings.tsx | 536 ++ .../dokploy/components/shared/code-editor.tsx | 5 +- apps/dokploy/drizzle/0148_strong_karma.sql | 1 + apps/dokploy/drizzle/0149_omniscient_bug.sql | 1 + apps/dokploy/drizzle/meta/0148_snapshot.json | 7467 +++++++++++++++++ apps/dokploy/drizzle/meta/0149_snapshot.json | 7467 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 14 + apps/dokploy/package.json | 15 +- apps/dokploy/pages/_app.tsx | 2 + apps/dokploy/pages/_error.tsx | 56 +- .../environment/[environmentId].tsx | 6 +- .../services/application/[applicationId].tsx | 6 +- .../services/compose/[composeId].tsx | 5 +- .../services/mariadb/[mariadbId].tsx | 5 +- .../services/mongo/[mongoId].tsx | 6 +- .../services/mysql/[mysqlId].tsx | 5 +- .../services/postgres/[postgresId].tsx | 6 +- .../services/redis/[redisId].tsx | 6 +- .../dashboard/settings/whitelabeling.tsx | 81 + apps/dokploy/pages/index.tsx | 11 +- apps/dokploy/pages/invitation.tsx | 17 +- apps/dokploy/pages/register.tsx | 17 +- apps/dokploy/pages/reset-password.tsx | 11 +- apps/dokploy/pages/send-reset-password.tsx | 12 +- apps/dokploy/server/api/root.ts | 2 + .../api/routers/proprietary/whitelabeling.ts | 88 + apps/dokploy/server/api/routers/user.ts | 5 +- apps/dokploy/utils/hooks/use-whitelabeling.ts | 13 + .../src/db/schema/web-server-settings.ts | 61 +- pnpm-lock.yaml | 23 + 36 files changed, 16148 insertions(+), 98 deletions(-) create mode 100644 apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx create mode 100644 apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx create mode 100644 apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx create mode 100644 apps/dokploy/drizzle/0148_strong_karma.sql create mode 100644 apps/dokploy/drizzle/0149_omniscient_bug.sql create mode 100644 apps/dokploy/drizzle/meta/0148_snapshot.json create mode 100644 apps/dokploy/drizzle/meta/0149_snapshot.json create mode 100644 apps/dokploy/pages/dashboard/settings/whitelabeling.tsx create mode 100644 apps/dokploy/server/api/routers/proprietary/whitelabeling.ts create mode 100644 apps/dokploy/utils/hooks/use-whitelabeling.ts diff --git a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts index b422279ca..eb99242c3 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -48,6 +48,21 @@ const baseSettings: WebServerSettings = { urlCallback: "", }, }, + whitelabelingConfig: { + appName: null, + appDescription: null, + logoUrl: null, + faviconUrl: null, + primaryColor: null, + customCss: null, + loginLogoUrl: null, + supportUrl: null, + docsUrl: null, + errorPageTitle: null, + errorPageDescription: null, + metaTitle: null, + footerText: null, + }, cleanupCacheApplications: false, cleanupCacheOnCompose: false, cleanupCacheOnPreviews: false, diff --git a/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx b/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx index f77983996..02f7f59f1 100644 --- a/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx +++ b/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx @@ -45,10 +45,12 @@ import { import { authClient } from "@/lib/auth-client"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; +import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling"; type User = typeof authClient.$Infer.Session.user; export const ImpersonationBar = () => { + const { config: whitelabeling } = useWhitelabelingPublic(); const [users, setUsers] = useState([]); const [selectedUser, setSelectedUser] = useState(null); const [isImpersonating, setIsImpersonating] = useState(false); @@ -180,7 +182,10 @@ export const ImpersonationBar = () => { )} >
- + {!isImpersonating ? (
diff --git a/apps/dokploy/components/layouts/onboarding-layout.tsx b/apps/dokploy/components/layouts/onboarding-layout.tsx index fff5413e0..c76c920fd 100644 --- a/apps/dokploy/components/layouts/onboarding-layout.tsx +++ b/apps/dokploy/components/layouts/onboarding-layout.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import type React from "react"; import { cn } from "@/lib/utils"; +import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling"; import { GithubIcon } from "../icons/data-tools-icons"; import { Logo } from "../shared/logo"; import { Button } from "../ui/button"; @@ -9,23 +10,28 @@ interface Props { children: React.ReactNode; } export const OnboardingLayout = ({ children }: Props) => { + const { config: whitelabeling } = useWhitelabelingPublic(); + const appName = whitelabeling?.appName || "Dokploy"; + const appDescription = + whitelabeling?.appDescription || + "\u201CThe Open Source alternative to Netlify, Vercel, Heroku.\u201D"; + const logoUrl = + whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined; + return (
- - Dokploy + + {appName}
-

- “The Open Source alternative to Netlify, Vercel, - Heroku.” -

+

{appDescription}

diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 6dea37f5b..487e5e2ee 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -23,6 +23,7 @@ import { Loader2, LogIn, type LucideIcon, + Palette, Package, PieChart, Rocket, @@ -422,6 +423,15 @@ const MENU: Menu = { isEnabled: ({ auth }) => !!(auth?.role === "owner" || auth?.role === "admin"), }, + { + isSingle: true, + title: "Whitelabeling", + url: "/dashboard/settings/whitelabeling", + icon: Palette, + // Only enabled for owners in non-cloud environments (enterprise) + isEnabled: ({ auth, isCloud }) => + !!(auth?.role === "owner" && !isCloud), + }, ], help: [ @@ -445,38 +455,33 @@ const MENU: Menu = { function createMenuForAuthUser(opts: { auth?: AuthQueryOutput; isCloud: boolean; + whitelabeling?: { + docsUrl?: string | null; + supportUrl?: string | null; + } | null; }): Menu { + const filterEnabled = boolean }>(items: readonly T[]): T[] => + items.filter((item) => + !item.isEnabled + ? true + : item.isEnabled({ auth: opts.auth, isCloud: opts.isCloud }), + ) as T[]; + + // Apply whitelabeling URL overrides to help items + const helpItems = filterEnabled(MENU.help).map((item) => { + if (opts.whitelabeling?.docsUrl && item.name === "Documentation") { + return { ...item, url: opts.whitelabeling.docsUrl }; + } + if (opts.whitelabeling?.supportUrl && item.name === "Support") { + return { ...item, url: opts.whitelabeling.supportUrl }; + } + return item; + }); + return { - // Filter the home items based on the user's role and permissions - // Calls the `isEnabled` function if it exists to determine if the item should be displayed - home: MENU.home.filter((item) => - !item.isEnabled - ? true - : item.isEnabled({ - auth: opts.auth, - isCloud: opts.isCloud, - }), - ), - // Filter the settings items based on the user's role and permissions - // Calls the `isEnabled` function if it exists to determine if the item should be displayed - settings: MENU.settings.filter((item) => - !item.isEnabled - ? true - : item.isEnabled({ - auth: opts.auth, - isCloud: opts.isCloud, - }), - ), - // Filter the help items based on the user's role and permissions - // Calls the `isEnabled` function if it exists to determine if the item should be displayed - help: MENU.help.filter((item) => - !item.isEnabled - ? true - : item.isEnabled({ - auth: opts.auth, - isCloud: opts.isCloud, - }), - ), + home: filterEnabled(MENU.home), + settings: filterEnabled(MENU.settings), + help: helpItems, }; } @@ -885,6 +890,10 @@ export default function Page({ children }: Props) { const pathname = usePathname(); const { data: auth } = api.user.get.useQuery(); const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); + const { data: whitelabeling } = api.whitelabeling.getPublic.useQuery( + undefined, + { staleTime: 5 * 60 * 1000, refetchOnWindowFocus: false }, + ); const includesProjects = pathname?.includes("/dashboard/project"); const { data: isCloud } = api.settings.isCloud.useQuery(); @@ -893,7 +902,7 @@ export default function Page({ children }: Props) { home: filteredHome, settings: filteredSettings, help, - } = createMenuForAuthUser({ auth, isCloud: !!isCloud }); + } = createMenuForAuthUser({ auth, isCloud: !!isCloud, whitelabeling }); const activeItem = findActiveNavItem( [...filteredHome, ...filteredSettings], @@ -1141,6 +1150,11 @@ export default function Page({ children }: Props) { + {whitelabeling?.footerText && ( +
+ {whitelabeling.footerText} +
+ )} {dokployVersion && ( <>
diff --git a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx new file mode 100644 index 000000000..f87268400 --- /dev/null +++ b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +interface WhitelabelingPreviewProps { + config: { + appName?: string; + logoUrl?: string; + primaryColor?: string; + footerText?: string; + }; +} + +export function WhitelabelingPreview({ config }: WhitelabelingPreviewProps) { + const appName = config.appName || "Dokploy"; + const primaryColor = config.primaryColor || "hsl(var(--primary))"; + + return ( + + + Live Preview + + A quick preview of how your branding changes will look. + + + +
+ {/* Simulated sidebar header */} +
+ {config.logoUrl ? ( + Preview Logo + ) : ( +
+ {appName.charAt(0).toUpperCase()} +
+ )} + {appName} +
+ + {/* Simulated content area */} +
+
+
+
+
+
+
+ Button +
+
+ Secondary +
+
+
+ + {/* Simulated footer */} + {config.footerText && ( +
+ {config.footerText} +
+ )} +
+ + + ); +} diff --git a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx new file mode 100644 index 000000000..651998cbf --- /dev/null +++ b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx @@ -0,0 +1,93 @@ +"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 || config.primaryColor) && ( +