mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-11 17:05:25 +02:00
@@ -48,6 +48,20 @@ const baseSettings: WebServerSettings = {
|
|||||||
urlCallback: "",
|
urlCallback: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
whitelabelingConfig: {
|
||||||
|
appName: null,
|
||||||
|
appDescription: null,
|
||||||
|
logoUrl: null,
|
||||||
|
faviconUrl: null,
|
||||||
|
customCss: null,
|
||||||
|
loginLogoUrl: null,
|
||||||
|
supportUrl: null,
|
||||||
|
docsUrl: null,
|
||||||
|
errorPageTitle: null,
|
||||||
|
errorPageDescription: null,
|
||||||
|
metaTitle: null,
|
||||||
|
footerText: null,
|
||||||
|
},
|
||||||
cleanupCacheApplications: false,
|
cleanupCacheApplications: false,
|
||||||
cleanupCacheOnCompose: false,
|
cleanupCacheOnCompose: false,
|
||||||
cleanupCacheOnPreviews: false,
|
cleanupCacheOnPreviews: false,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Clock,
|
Clock,
|
||||||
|
Copy,
|
||||||
Loader2,
|
Loader2,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
RocketIcon,
|
RocketIcon,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
@@ -97,6 +99,12 @@ export const ShowDeployments = ({
|
|||||||
new Set(),
|
new Set(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const webhookUrl = useMemo(
|
||||||
|
() =>
|
||||||
|
`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`,
|
||||||
|
[url, refreshToken, type],
|
||||||
|
);
|
||||||
|
|
||||||
const MAX_DESCRIPTION_LENGTH = 200;
|
const MAX_DESCRIPTION_LENGTH = 200;
|
||||||
|
|
||||||
const truncateDescription = (description: string): string => {
|
const truncateDescription = (description: string): string => {
|
||||||
@@ -224,11 +232,27 @@ export const ShowDeployments = ({
|
|||||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||||
<span>Webhook URL: </span>
|
<span>Webhook URL: </span>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<span className="break-all text-muted-foreground">
|
<Badge
|
||||||
{`${url}/api/deploy${
|
role="button"
|
||||||
type === "compose" ? "/compose" : ""
|
tabIndex={0}
|
||||||
}/${refreshToken}`}
|
aria-label="Copy webhook URL to clipboard"
|
||||||
</span>
|
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer whitespace-normal break-all"
|
||||||
|
variant="outline"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
copy(webhookUrl);
|
||||||
|
toast.success("Copied to clipboard.");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
copy(webhookUrl);
|
||||||
|
toast.success("Copied to clipboard.");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{webhookUrl}
|
||||||
|
<Copy className="h-4 w-4 ml-2" />
|
||||||
|
</Badge>
|
||||||
{(type === "application" || type === "compose") && (
|
{(type === "application" || type === "compose") && (
|
||||||
<RefreshToken id={id} type={type} />
|
<RefreshToken id={id} type={type} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -45,10 +45,12 @@ import {
|
|||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type User = typeof authClient.$Infer.Session.user;
|
type User = typeof authClient.$Infer.Session.user;
|
||||||
|
|
||||||
export const ImpersonationBar = () => {
|
export const ImpersonationBar = () => {
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
const [isImpersonating, setIsImpersonating] = useState(false);
|
const [isImpersonating, setIsImpersonating] = useState(false);
|
||||||
@@ -180,7 +182,10 @@ export const ImpersonationBar = () => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
|
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
|
||||||
<Logo className="w-10 h-10" />
|
<Logo
|
||||||
|
className="w-10 h-10"
|
||||||
|
logoUrl={whitelabeling?.logoUrl || undefined}
|
||||||
|
/>
|
||||||
{!isImpersonating ? (
|
{!isImpersonating ? (
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
import { GithubIcon } from "../icons/data-tools-icons";
|
import { GithubIcon } from "../icons/data-tools-icons";
|
||||||
import { Logo } from "../shared/logo";
|
import { Logo } from "../shared/logo";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
@@ -9,23 +10,28 @@ interface Props {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
export const OnboardingLayout = ({ children }: Props) => {
|
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 (
|
return (
|
||||||
<div className="container relative min-h-svh flex-col items-center justify-center flex lg:max-w-none lg:grid lg:grid-cols-2 lg:px-0 w-full">
|
<div className="container relative min-h-svh flex-col items-center justify-center flex lg:max-w-none lg:grid lg:grid-cols-2 lg:px-0 w-full">
|
||||||
<div className="relative hidden h-full flex-col p-10 text-primary dark:border-r lg:flex">
|
<div className="relative hidden h-full flex-col p-10 text-primary dark:border-r lg:flex">
|
||||||
<div className="absolute inset-0 bg-muted" />
|
<div className="absolute inset-0 bg-muted" />
|
||||||
<Link
|
<Link
|
||||||
href="https://dokploy.com"
|
href="/"
|
||||||
className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary"
|
className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary"
|
||||||
>
|
>
|
||||||
<Logo className="size-10" />
|
<Logo className="size-10" logoUrl={logoUrl} />
|
||||||
Dokploy
|
{appName}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="relative z-20 mt-auto">
|
<div className="relative z-20 mt-auto">
|
||||||
<blockquote className="space-y-2">
|
<blockquote className="space-y-2">
|
||||||
<p className="text-lg text-primary">
|
<p className="text-lg text-primary">{appDescription}</p>
|
||||||
“The Open Source alternative to Netlify, Vercel,
|
|
||||||
Heroku.”
|
|
||||||
</p>
|
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
LogIn,
|
LogIn,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Package,
|
Package,
|
||||||
|
Palette,
|
||||||
PieChart,
|
PieChart,
|
||||||
Rocket,
|
Rocket,
|
||||||
Server,
|
Server,
|
||||||
@@ -422,6 +423,14 @@ const MENU: Menu = {
|
|||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ auth }) =>
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
!!(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: [
|
help: [
|
||||||
@@ -445,38 +454,39 @@ const MENU: Menu = {
|
|||||||
function createMenuForAuthUser(opts: {
|
function createMenuForAuthUser(opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
|
whitelabeling?: {
|
||||||
|
docsUrl?: string | null;
|
||||||
|
supportUrl?: string | null;
|
||||||
|
} | null;
|
||||||
}): Menu {
|
}): Menu {
|
||||||
|
const filterEnabled = <
|
||||||
|
T extends {
|
||||||
|
isEnabled?: (o: { auth?: AuthQueryOutput; isCloud: boolean }) => 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 {
|
return {
|
||||||
// Filter the home items based on the user's role and permissions
|
home: filterEnabled(MENU.home),
|
||||||
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
|
settings: filterEnabled(MENU.settings),
|
||||||
home: MENU.home.filter((item) =>
|
help: helpItems,
|
||||||
!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,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,6 +895,10 @@ export default function Page({ children }: Props) {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
|
const { data: whitelabeling } = api.whitelabeling.get.useQuery(undefined, {
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
const includesProjects = pathname?.includes("/dashboard/project");
|
const includesProjects = pathname?.includes("/dashboard/project");
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
@@ -893,7 +907,7 @@ export default function Page({ children }: Props) {
|
|||||||
home: filteredHome,
|
home: filteredHome,
|
||||||
settings: filteredSettings,
|
settings: filteredSettings,
|
||||||
help,
|
help,
|
||||||
} = createMenuForAuthUser({ auth, isCloud: !!isCloud });
|
} = createMenuForAuthUser({ auth, isCloud: !!isCloud, whitelabeling });
|
||||||
|
|
||||||
const activeItem = findActiveNavItem(
|
const activeItem = findActiveNavItem(
|
||||||
[...filteredHome, ...filteredSettings],
|
[...filteredHome, ...filteredSettings],
|
||||||
@@ -1141,6 +1155,11 @@ export default function Page({ children }: Props) {
|
|||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UserNav />
|
<UserNav />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
{whitelabeling?.footerText && (
|
||||||
|
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
||||||
|
{whitelabeling.footerText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{dokployVersion && (
|
{dokployVersion && (
|
||||||
<>
|
<>
|
||||||
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
|
interface WhitelabelingPreviewProps {
|
||||||
|
config: {
|
||||||
|
appName?: string;
|
||||||
|
logoUrl?: string;
|
||||||
|
footerText?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WhitelabelingPreview({ config }: WhitelabelingPreviewProps) {
|
||||||
|
const appName = config.appName || "Dokploy";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Live Preview</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
A quick preview of how your branding changes will look.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="rounded-lg border overflow-hidden">
|
||||||
|
{/* Simulated sidebar header */}
|
||||||
|
<div className="flex items-center gap-3 p-4 border-b bg-sidebar">
|
||||||
|
{config.logoUrl ? (
|
||||||
|
<img
|
||||||
|
src={config.logoUrl}
|
||||||
|
alt="Preview Logo"
|
||||||
|
className="size-8 rounded-sm object-contain"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="size-8 rounded-sm flex items-center justify-center bg-primary text-primary-foreground font-bold text-sm">
|
||||||
|
{appName.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="font-semibold text-sm">{appName}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Simulated content area */}
|
||||||
|
<div className="p-4 bg-background">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div className="h-2 w-16 rounded-full bg-primary" />
|
||||||
|
<div className="h-2 w-24 rounded-full bg-muted" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="px-3 py-1.5 rounded-md text-xs bg-primary text-primary-foreground font-medium">
|
||||||
|
Button
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-1.5 rounded-md text-xs border font-medium">
|
||||||
|
Secondary
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Simulated footer */}
|
||||||
|
{config.footerText && (
|
||||||
|
<div className="px-4 py-2 border-t text-xs text-muted-foreground text-center bg-sidebar">
|
||||||
|
{config.footerText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"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 (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
{config.metaTitle && <title>{config.metaTitle}</title>}
|
||||||
|
{config.faviconUrl && <link rel="icon" href={config.faviconUrl} />}
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
{config.customCss && (
|
||||||
|
<style
|
||||||
|
id="whitelabeling-styles"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: config.customCss,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,589 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
|
import { Loader2, RotateCcw } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
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";
|
||||||
|
import { WhitelabelingPreview } from "./whitelabeling-preview";
|
||||||
|
|
||||||
|
const safeUrlField = z
|
||||||
|
.string()
|
||||||
|
.refine((val) => val === "" || /^https?:\/\//i.test(val), {
|
||||||
|
message: "Only http:// and https:// URLs are allowed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
appName: z.string(),
|
||||||
|
appDescription: z.string(),
|
||||||
|
logoUrl: safeUrlField,
|
||||||
|
faviconUrl: safeUrlField,
|
||||||
|
customCss: z.string(),
|
||||||
|
loginLogoUrl: safeUrlField,
|
||||||
|
supportUrl: safeUrlField,
|
||||||
|
docsUrl: safeUrlField,
|
||||||
|
errorPageTitle: z.string(),
|
||||||
|
errorPageDescription: z.string(),
|
||||||
|
metaTitle: z.string(),
|
||||||
|
footerText: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const DEFAULT_CSS_TEMPLATE = `/* ============================================
|
||||||
|
Dokploy Default Theme - CSS Variables
|
||||||
|
Modify these values to customize your instance.
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* ---------- Light Mode ---------- */
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
|
--primary: 240 5.9% 10%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 50.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 240 10% 3.9%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--sidebar-background: 0 0% 98%;
|
||||||
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 98%;
|
||||||
|
--sidebar-accent: 240 4.8% 95.9%;
|
||||||
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
|
--sidebar-border: 220 13% 91%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
|
||||||
|
/* Charts */
|
||||||
|
--chart-1: 173 58% 39%;
|
||||||
|
--chart-2: 12 76% 61%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Dark Mode ---------- */
|
||||||
|
.dark {
|
||||||
|
--background: 0 0% 0%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--card: 240 4% 10%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--popover: 240 10% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--muted: 240 4% 10%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
|
||||||
|
--accent: 240 3.7% 15.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 50.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 4% 10%;
|
||||||
|
--ring: 240 4.9% 83.9%;
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--sidebar-background: 240 5.9% 10%;
|
||||||
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-primary: 224.3 76.3% 48%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 240 3.7% 15.9%;
|
||||||
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
|
||||||
|
/* Charts */
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 340 75% 55%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 160 60% 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Custom Styles ---------- */
|
||||||
|
/* Add your own CSS rules below */
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function WhitelabelingSettings() {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isPending: isLoading,
|
||||||
|
refetch,
|
||||||
|
} = api.whitelabeling.get.useQuery();
|
||||||
|
|
||||||
|
const { mutateAsync: updateWhitelabeling, isPending: isUpdating } =
|
||||||
|
api.whitelabeling.update.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: resetWhitelabeling, isPending: isResetting } =
|
||||||
|
api.whitelabeling.reset.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<FormSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
appName: "",
|
||||||
|
appDescription: "",
|
||||||
|
logoUrl: "",
|
||||||
|
faviconUrl: "",
|
||||||
|
customCss: "",
|
||||||
|
loginLogoUrl: "",
|
||||||
|
supportUrl: "",
|
||||||
|
docsUrl: "",
|
||||||
|
errorPageTitle: "",
|
||||||
|
errorPageDescription: "",
|
||||||
|
metaTitle: "",
|
||||||
|
footerText: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
appName: data.appName ?? "",
|
||||||
|
appDescription: data.appDescription ?? "",
|
||||||
|
logoUrl: data.logoUrl ?? "",
|
||||||
|
faviconUrl: data.faviconUrl ?? "",
|
||||||
|
customCss: data.customCss ?? "",
|
||||||
|
loginLogoUrl: data.loginLogoUrl ?? "",
|
||||||
|
supportUrl: data.supportUrl ?? "",
|
||||||
|
docsUrl: data.docsUrl ?? "",
|
||||||
|
errorPageTitle: data.errorPageTitle ?? "",
|
||||||
|
errorPageDescription: data.errorPageDescription ?? "",
|
||||||
|
metaTitle: data.metaTitle ?? "",
|
||||||
|
footerText: data.footerText ?? "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 justify-center min-h-[25vh]">
|
||||||
|
<Loader2 className="size-6 text-muted-foreground animate-spin" />
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Loading whitelabeling settings...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (values: FormSchema) => {
|
||||||
|
await updateWhitelabeling({
|
||||||
|
whitelabelingConfig: {
|
||||||
|
appName: values.appName || null,
|
||||||
|
appDescription: values.appDescription || null,
|
||||||
|
logoUrl: values.logoUrl || null,
|
||||||
|
faviconUrl: values.faviconUrl || null,
|
||||||
|
customCss: values.customCss || null,
|
||||||
|
loginLogoUrl: values.loginLogoUrl || null,
|
||||||
|
supportUrl: values.supportUrl || null,
|
||||||
|
docsUrl: values.docsUrl || null,
|
||||||
|
errorPageTitle: values.errorPageTitle || null,
|
||||||
|
errorPageDescription: values.errorPageDescription || null,
|
||||||
|
metaTitle: values.metaTitle || null,
|
||||||
|
footerText: values.footerText || null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Whitelabeling settings updated");
|
||||||
|
await refetch();
|
||||||
|
await utils.whitelabeling.getPublic.invalidate();
|
||||||
|
await utils.whitelabeling.get.invalidate();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(
|
||||||
|
error?.message || "Failed to update whitelabeling settings",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = async () => {
|
||||||
|
await resetWhitelabeling()
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Whitelabeling settings reset to defaults");
|
||||||
|
await refetch();
|
||||||
|
await utils.whitelabeling.getPublic.invalidate();
|
||||||
|
await utils.whitelabeling.get.invalidate();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error?.message || "Failed to reset whitelabeling settings");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="flex flex-col gap-6"
|
||||||
|
>
|
||||||
|
{/* Branding Section */}
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Branding</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Customize the application name, logos, and favicon to match your
|
||||||
|
brand identity.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Application Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Dokploy" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Replaces "Dokploy" across the entire interface.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appDescription"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Application Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="The Open Source alternative to Netlify, Vercel, Heroku."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Tagline shown on the login/onboarding pages. Defaults to
|
||||||
|
the standard Dokploy description if empty.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="logoUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Logo URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://example.com/logo.svg"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Main logo shown in the sidebar and header. Recommended
|
||||||
|
size: 128x128px.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="loginLogoUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Login Page Logo URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://example.com/login-logo.svg"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Logo displayed on the login page. If empty, the main logo
|
||||||
|
is used.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="faviconUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Favicon URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://example.com/favicon.ico"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Browser tab icon. Supports .ico, .png, and .svg formats.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Appearance Section */}
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Appearance</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Customize the look and feel of the application with custom CSS.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="customCss"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Custom CSS</FormLabel>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
form.setValue("customCss", DEFAULT_CSS_TEMPLATE);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Load Default Styles
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<div className="max-h-[350px] overflow-auto">
|
||||||
|
<CodeEditor
|
||||||
|
language="css"
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
placeholder="/* Click 'Load Default Styles' to start with the base theme variables */"
|
||||||
|
lineWrapping
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Inject custom CSS styles globally. Click "Load Default
|
||||||
|
Styles" to get the base theme CSS variables as a starting
|
||||||
|
point.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Metadata & Links Section */}
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Metadata & Links</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Customize the page title, footer text, and sidebar links.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="metaTitle"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Page Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Dokploy" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Browser tab title. Defaults to "Dokploy" if empty.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="footerText"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Footer Text</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Powered by Your Company" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Custom text displayed in the footer area.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="supportUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Support URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://support.example.com"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Custom URL for the "Support" link in the sidebar.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="docsUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Documentation URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://docs.example.com"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Custom URL for the "Documentation" link in the sidebar.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Error Pages Section */}
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Error Pages</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Customize the error page messages shown to users.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="errorPageTitle"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Error Page Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Something went wrong" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="errorPageDescription"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Error Page Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="We're sorry, but an unexpected error occurred. Please try again later."
|
||||||
|
className="min-h-[80px]"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<DialogAction
|
||||||
|
title="Reset Whitelabeling"
|
||||||
|
description="Are you sure you want to reset all whitelabeling settings to their defaults? This action cannot be undone."
|
||||||
|
type="destructive"
|
||||||
|
onClick={handleReset}
|
||||||
|
>
|
||||||
|
<Button variant="outline" type="button" isLoading={isResetting}>
|
||||||
|
<RotateCcw className="size-4 mr-2" />
|
||||||
|
Reset to Defaults
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
|
||||||
|
<Button type="submit" isLoading={isUpdating} disabled={isUpdating}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* Live Preview */}
|
||||||
|
<WhitelabelingPreview config={form.watch()} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
type CompletionContext,
|
type CompletionContext,
|
||||||
type CompletionResult,
|
type CompletionResult,
|
||||||
} from "@codemirror/autocomplete";
|
} from "@codemirror/autocomplete";
|
||||||
|
import { css } from "@codemirror/lang-css";
|
||||||
import { json } from "@codemirror/lang-json";
|
import { json } from "@codemirror/lang-json";
|
||||||
import { yaml } from "@codemirror/lang-yaml";
|
import { yaml } from "@codemirror/lang-yaml";
|
||||||
import { StreamLanguage } from "@codemirror/language";
|
import { StreamLanguage } from "@codemirror/language";
|
||||||
@@ -131,7 +132,7 @@ function dockerComposeComplete(
|
|||||||
interface Props extends ReactCodeMirrorProps {
|
interface Props extends ReactCodeMirrorProps {
|
||||||
wrapperClassName?: string;
|
wrapperClassName?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
language?: "yaml" | "json" | "properties" | "shell";
|
language?: "yaml" | "json" | "properties" | "shell" | "css";
|
||||||
lineWrapping?: boolean;
|
lineWrapping?: boolean;
|
||||||
lineNumbers?: boolean;
|
lineNumbers?: boolean;
|
||||||
}
|
}
|
||||||
@@ -162,9 +163,11 @@ export const CodeEditor = ({
|
|||||||
? yaml()
|
? yaml()
|
||||||
: language === "json"
|
: language === "json"
|
||||||
? json()
|
? json()
|
||||||
: language === "shell"
|
: language === "css"
|
||||||
? StreamLanguage.define(shell)
|
? css()
|
||||||
: StreamLanguage.define(properties),
|
: language === "shell"
|
||||||
|
? StreamLanguage.define(shell)
|
||||||
|
: StreamLanguage.define(properties),
|
||||||
props.lineWrapping ? EditorView.lineWrapping : [],
|
props.lineWrapping ? EditorView.lineWrapping : [],
|
||||||
language === "yaml"
|
language === "yaml"
|
||||||
? autocompletion({
|
? autocompletion({
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0148_futuristic_bullseye.sql
Normal file
1
apps/dokploy/drizzle/0148_futuristic_bullseye.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "webServerSettings" ADD COLUMN "whitelabelingConfig" jsonb DEFAULT '{"appName":null,"appDescription":null,"logoUrl":null,"faviconUrl":null,"customCss":null,"loginLogoUrl":null,"supportUrl":null,"docsUrl":null,"errorPageTitle":null,"errorPageDescription":null,"metaTitle":null,"footerText":null}'::jsonb;
|
||||||
7467
apps/dokploy/drizzle/meta/0148_snapshot.json
Normal file
7467
apps/dokploy/drizzle/meta/0148_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1037,6 +1037,13 @@
|
|||||||
"when": 1771830695385,
|
"when": 1771830695385,
|
||||||
"tag": "0147_right_lake",
|
"tag": "0147_right_lake",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 148,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773129798212,
|
||||||
|
"tag": "0148_futuristic_bullseye",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.28.5",
|
"version": "v0.28.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -39,8 +39,6 @@
|
|||||||
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
|
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"resend": "^6.0.2",
|
|
||||||
"@better-auth/sso": "1.5.0-beta.16",
|
|
||||||
"@ai-sdk/anthropic": "^3.0.44",
|
"@ai-sdk/anthropic": "^3.0.44",
|
||||||
"@ai-sdk/azure": "^3.0.30",
|
"@ai-sdk/azure": "^3.0.30",
|
||||||
"@ai-sdk/cohere": "^3.0.21",
|
"@ai-sdk/cohere": "^3.0.21",
|
||||||
@@ -48,7 +46,9 @@
|
|||||||
"@ai-sdk/mistral": "^3.0.20",
|
"@ai-sdk/mistral": "^3.0.20",
|
||||||
"@ai-sdk/openai": "^3.0.29",
|
"@ai-sdk/openai": "^3.0.29",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||||
|
"@better-auth/sso": "1.5.0-beta.16",
|
||||||
"@codemirror/autocomplete": "^6.18.6",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.2",
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
"@codemirror/language": "^6.11.0",
|
"@codemirror/language": "^6.11.0",
|
||||||
@@ -103,7 +103,6 @@
|
|||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.67.3",
|
"bullmq": "5.67.3",
|
||||||
"shell-quote": "^1.8.1",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^0.2.1",
|
"cmdk": "^0.2.1",
|
||||||
@@ -140,6 +139,9 @@
|
|||||||
"react-hook-form": "^7.71.2",
|
"react-hook-form": "^7.71.2",
|
||||||
"react-markdown": "^9.1.0",
|
"react-markdown": "^9.1.0",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
|
"resend": "^6.0.2",
|
||||||
|
"semver": "7.7.3",
|
||||||
|
"shell-quote": "^1.8.1",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"ssh2": "1.15.0",
|
"ssh2": "1.15.0",
|
||||||
@@ -155,12 +157,9 @@
|
|||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"yaml": "2.8.1",
|
"yaml": "2.8.1",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
"zod-form-data": "^3.0.1",
|
"zod-form-data": "^3.0.1"
|
||||||
"semver": "7.7.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/semver": "7.7.1",
|
|
||||||
"@types/shell-quote": "^1.7.5",
|
|
||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
@@ -172,6 +171,8 @@
|
|||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/semver": "7.7.1",
|
||||||
|
"@types/shell-quote": "^1.7.5",
|
||||||
"@types/ssh2": "1.15.1",
|
"@types/ssh2": "1.15.1",
|
||||||
"@types/swagger-ui-react": "^4.19.0",
|
"@types/swagger-ui-react": "^4.19.0",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ThemeProvider } from "next-themes";
|
|||||||
import NextTopLoader from "nextjs-toploader";
|
import NextTopLoader from "nextjs-toploader";
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
import { SearchCommand } from "@/components/dashboard/search-command";
|
import { SearchCommand } from "@/components/dashboard/search-command";
|
||||||
|
import { WhitelabelingProvider } from "@/components/proprietary/whitelabeling/whitelabeling-provider";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ const MyApp = ({
|
|||||||
forcedTheme={Component.theme}
|
forcedTheme={Component.theme}
|
||||||
>
|
>
|
||||||
<NextTopLoader color="hsl(var(--sidebar-ring))" />
|
<NextTopLoader color="hsl(var(--sidebar-ring))" />
|
||||||
|
<WhitelabelingProvider />
|
||||||
<Toaster richColors />
|
<Toaster richColors />
|
||||||
<SearchCommand />
|
<SearchCommand />
|
||||||
{getLayout(<Component {...pageProps} />)}
|
{getLayout(<Component {...pageProps} />)}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { NextPageContext } from "next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Logo } from "@/components/shared/logo";
|
import { Logo } from "@/components/shared/logo";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
@@ -10,18 +11,20 @@ interface Props {
|
|||||||
|
|
||||||
export default function Custom404({ statusCode, error }: Props) {
|
export default function Custom404({ statusCode, error }: Props) {
|
||||||
const displayStatusCode = statusCode || 400;
|
const displayStatusCode = statusCode || 400;
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
|
const logoUrl = whitelabeling?.logoUrl || undefined;
|
||||||
|
const errorTitle = whitelabeling?.errorPageTitle;
|
||||||
|
const errorDescription = whitelabeling?.errorPageDescription;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen">
|
<div className="h-screen">
|
||||||
<div className="max-w-[50rem] flex flex-col mx-auto size-full">
|
<div className="max-w-[50rem] flex flex-col mx-auto size-full">
|
||||||
<header className="mb-auto flex justify-center z-50 w-full py-4">
|
<header className="mb-auto flex justify-center z-50 w-full py-4">
|
||||||
<nav className="px-4 sm:px-6 lg:px-8" aria-label="Global">
|
<nav className="px-4 sm:px-6 lg:px-8" aria-label="Global">
|
||||||
<Link
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
href="https://dokploy.com"
|
<Logo logoUrl={logoUrl} />
|
||||||
target="_blank"
|
<span className="font-medium text-sm">{appName}</span>
|
||||||
className="flex flex-row items-center gap-2"
|
|
||||||
>
|
|
||||||
<Logo />
|
|
||||||
<span className="font-medium text-sm">Dokploy</span>
|
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -30,19 +33,18 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
<h1 className="block text-7xl font-bold text-primary sm:text-9xl">
|
<h1 className="block text-7xl font-bold text-primary sm:text-9xl">
|
||||||
{displayStatusCode}
|
{displayStatusCode}
|
||||||
</h1>
|
</h1>
|
||||||
{/* <AlertBlock className="max-w-xs mx-auto">
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Oops, something went wrong.
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Sorry, we couldn't find your page.
|
|
||||||
</p>
|
|
||||||
</AlertBlock> */}
|
|
||||||
<p className="mt-3 text-muted-foreground">
|
<p className="mt-3 text-muted-foreground">
|
||||||
{statusCode === 404
|
{errorTitle
|
||||||
? "Sorry, we couldn't find your page."
|
? errorTitle
|
||||||
: "Oops, something went wrong."}
|
: statusCode === 404
|
||||||
|
? "Sorry, we couldn't find your page."
|
||||||
|
: "Oops, something went wrong."}
|
||||||
</p>
|
</p>
|
||||||
|
{errorDescription && (
|
||||||
|
<p className="mt-2 text-muted-foreground text-sm">
|
||||||
|
{errorDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mt-3 text-red-500">
|
<div className="mt-3 text-red-500">
|
||||||
<p>{error.message}</p>
|
<p>{error.message}</p>
|
||||||
@@ -80,13 +82,17 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
<footer className="mt-auto text-center py-5">
|
<footer className="mt-auto text-center py-5">
|
||||||
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
<Link
|
{whitelabeling?.footerText ? (
|
||||||
href="https://github.com/Dokploy/dokploy/issues"
|
whitelabeling.footerText
|
||||||
target="_blank"
|
) : (
|
||||||
className="underline hover:text-primary transition-colors"
|
<Link
|
||||||
>
|
href="https://github.com/Dokploy/dokploy/issues"
|
||||||
Submit Log in issue on Github
|
target="_blank"
|
||||||
</Link>
|
className="underline hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
Submit Log in issue on Github
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -358,7 +358,8 @@ export default async function handler(
|
|||||||
const shouldCreateDeployment =
|
const shouldCreateDeployment =
|
||||||
action === "opened" ||
|
action === "opened" ||
|
||||||
action === "synchronize" ||
|
action === "synchronize" ||
|
||||||
action === "reopened";
|
action === "reopened" ||
|
||||||
|
action === "labeled";
|
||||||
|
|
||||||
const repository = githubBody?.repository?.name;
|
const repository = githubBody?.repository?.name;
|
||||||
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
@@ -370,6 +371,8 @@ const EnvironmentPage = (
|
|||||||
{ projectId: selectedTargetProject },
|
{ projectId: selectedTargetProject },
|
||||||
{ enabled: !!selectedTargetProject },
|
{ enabled: !!selectedTargetProject },
|
||||||
);
|
);
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
|
|
||||||
const emptyServices =
|
const emptyServices =
|
||||||
!currentEnvironment ||
|
!currentEnvironment ||
|
||||||
@@ -871,7 +874,8 @@ const EnvironmentPage = (
|
|||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Environment: {currentEnvironment.name} | {projectData?.name} | Dokploy
|
Environment: {currentEnvironment.name} | {projectData?.name} |{" "}
|
||||||
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import {
|
|||||||
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState =
|
type TabState =
|
||||||
| "projects"
|
| "projects"
|
||||||
@@ -95,6 +96,8 @@ const Service = (
|
|||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.project?.projectId || "",
|
projectId: data?.environment?.project?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -122,7 +125,8 @@ const Service = (
|
|||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Application: {data?.name} - {data?.environment.project.name} | Dokploy
|
Application: {data?.name} - {data?.environment.project.name} |{" "}
|
||||||
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState =
|
type TabState =
|
||||||
| "projects"
|
| "projects"
|
||||||
@@ -84,6 +85,8 @@ const Service = (
|
|||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -111,7 +114,7 @@ const Service = (
|
|||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Compose: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Compose: {data?.name} - {data?.environment?.project?.name} | {appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -65,6 +66,8 @@ const Mariadb = (
|
|||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -94,7 +97,7 @@ const Mariadb = (
|
|||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} |
|
Database: {data?.name} - {data?.environment?.project?.name} |
|
||||||
Dokploy
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -64,6 +65,8 @@ const Mongo = (
|
|||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -91,7 +94,8 @@ const Mongo = (
|
|||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||||
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -63,6 +64,8 @@ const MySql = (
|
|||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -92,7 +95,7 @@ const MySql = (
|
|||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} |
|
Database: {data?.name} - {data?.environment?.project?.name} |
|
||||||
Dokploy
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -63,6 +64,8 @@ const Postgresql = (
|
|||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -90,7 +93,8 @@ const Postgresql = (
|
|||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||||
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "advanced";
|
||||||
|
|
||||||
@@ -63,6 +64,8 @@ const Redis = (
|
|||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -90,7 +93,8 @@ const Redis = (
|
|||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||||
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
81
apps/dokploy/pages/dashboard/settings/whitelabeling.tsx
Normal file
81
apps/dokploy/pages/dashboard/settings/whitelabeling.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
|
||||||
|
import { WhitelabelingSettings } from "@/components/proprietary/whitelabeling/whitelabeling-settings";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||||
|
<div className="rounded-xl bg-background shadow-md">
|
||||||
|
<div className="p-6">
|
||||||
|
<EnterpriseFeatureGate
|
||||||
|
lockedProps={{
|
||||||
|
title: "Enterprise Whitelabeling",
|
||||||
|
description:
|
||||||
|
"Whitelabeling allows you to fully customize logos, colors, CSS, error pages, and more. Add a valid license to configure it.",
|
||||||
|
ctaLabel: "Go to License",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WhitelabelingSettings />
|
||||||
|
</EnterpriseFeatureGate>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
||||||
|
Page.getLayout = (page: ReactElement) => {
|
||||||
|
return <DashboardLayout metaName="Whitelabeling">{page}</DashboardLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||||
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (user.role !== "owner") {
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
const LoginSchema = z.object({
|
const LoginSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
@@ -58,6 +59,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export default function Home({ IS_CLOUD }: Props) {
|
export default function Home({ IS_CLOUD }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
|
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
|
||||||
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||||
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
||||||
@@ -216,7 +218,14 @@ export default function Home({ IS_CLOUD }: Props) {
|
|||||||
<div className="flex flex-col space-y-2 text-center">
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
<div className="flex flex-row items-center justify-center gap-2">
|
<div className="flex flex-row items-center justify-center gap-2">
|
||||||
<Logo className="size-12" />
|
<Logo
|
||||||
|
className="size-12"
|
||||||
|
logoUrl={
|
||||||
|
whitelabeling?.loginLogoUrl ||
|
||||||
|
whitelabeling?.logoUrl ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
Sign in
|
Sign in
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -82,6 +83,7 @@ const Invitation = ({
|
|||||||
userAlreadyExists,
|
userAlreadyExists,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const { data } = api.user.getUserByToken.useQuery(
|
const { data } = api.user.getUserByToken.useQuery(
|
||||||
{
|
{
|
||||||
token,
|
token,
|
||||||
@@ -148,12 +150,15 @@ const Invitation = ({
|
|||||||
<div className="flex h-screen w-full items-center justify-center ">
|
<div className="flex h-screen w-full items-center justify-center ">
|
||||||
<div className="flex flex-col items-center gap-4 w-full">
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
<Link
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
href="https://dokploy.com"
|
<Logo
|
||||||
target="_blank"
|
className="size-12"
|
||||||
className="flex flex-row items-center gap-2"
|
logoUrl={
|
||||||
>
|
whitelabeling?.loginLogoUrl ||
|
||||||
<Logo className="size-12" />
|
whitelabeling?.logoUrl ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
Invitation
|
Invitation
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -77,6 +78,7 @@ interface Props {
|
|||||||
|
|
||||||
const Register = ({ isCloud }: Props) => {
|
const Register = ({ isCloud }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [data, setData] = useState<any>(null);
|
const [data, setData] = useState<any>(null);
|
||||||
@@ -123,12 +125,15 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
<div className="flex w-full items-center justify-center ">
|
<div className="flex w-full items-center justify-center ">
|
||||||
<div className="flex flex-col items-center gap-4 w-full">
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
<Link
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
href="https://dokploy.com"
|
<Logo
|
||||||
target="_blank"
|
className="size-12"
|
||||||
className="flex flex-row items-center gap-2"
|
logoUrl={
|
||||||
>
|
whitelabeling?.loginLogoUrl ||
|
||||||
<Logo className="size-12" />
|
whitelabeling?.logoUrl ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
{isCloud ? "Sign Up" : "Setup the server"}
|
{isCloud ? "Sign Up" : "Setup the server"}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
const loginSchema = z
|
const loginSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -53,6 +54,7 @@ interface Props {
|
|||||||
tokenResetPassword: string;
|
tokenResetPassword: string;
|
||||||
}
|
}
|
||||||
export default function Home({ tokenResetPassword }: Props) {
|
export default function Home({ tokenResetPassword }: Props) {
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const [token, setToken] = useState<string | null>(tokenResetPassword);
|
const [token, setToken] = useState<string | null>(tokenResetPassword);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -97,7 +99,14 @@ export default function Home({ tokenResetPassword }: Props) {
|
|||||||
<div className="flex flex-col items-center gap-4 w-full">
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
<CardTitle className="text-2xl font-bold flex flex-row gap-2 items-center">
|
<CardTitle className="text-2xl font-bold flex flex-row gap-2 items-center">
|
||||||
<Link href="/" className="flex flex-row items-center gap-2">
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
<Logo className="size-12" />
|
<Logo
|
||||||
|
className="size-12"
|
||||||
|
logoUrl={
|
||||||
|
whitelabeling?.loginLogoUrl ||
|
||||||
|
whitelabeling?.logoUrl ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
Reset Password
|
Reset Password
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const loginSchema = z.object({
|
||||||
email: z
|
email: z
|
||||||
@@ -42,6 +43,7 @@ type AuthResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const [temp, _setTemp] = useState<AuthResponse>({
|
const [temp, _setTemp] = useState<AuthResponse>({
|
||||||
is2FAEnabled: false,
|
is2FAEnabled: false,
|
||||||
authId: "",
|
authId: "",
|
||||||
@@ -81,8 +83,14 @@ export default function Home() {
|
|||||||
<div className="flex w-full items-center justify-center ">
|
<div className="flex w-full items-center justify-center ">
|
||||||
<div className="flex flex-col items-center gap-4 w-full">
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
<Link href="/" className="flex flex-row items-center gap-2">
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
<Logo />
|
<Logo
|
||||||
<span className="font-medium text-sm">Dokploy</span>
|
logoUrl={
|
||||||
|
whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="font-medium text-sm">
|
||||||
|
{whitelabeling?.appName || "Dokploy"}
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { organizationRouter } from "./routers/organization";
|
|||||||
import { patchRouter } from "./routers/patch";
|
import { patchRouter } from "./routers/patch";
|
||||||
import { licenseKeyRouter } from "./routers/proprietary/license-key";
|
import { licenseKeyRouter } from "./routers/proprietary/license-key";
|
||||||
import { ssoRouter } from "./routers/proprietary/sso";
|
import { ssoRouter } from "./routers/proprietary/sso";
|
||||||
|
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
|
||||||
import { portRouter } from "./routers/port";
|
import { portRouter } from "./routers/port";
|
||||||
import { postgresRouter } from "./routers/postgres";
|
import { postgresRouter } from "./routers/postgres";
|
||||||
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
||||||
@@ -87,6 +88,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
organization: organizationRouter,
|
organization: organizationRouter,
|
||||||
licenseKey: licenseKeyRouter,
|
licenseKey: licenseKeyRouter,
|
||||||
sso: ssoRouter,
|
sso: ssoRouter,
|
||||||
|
whitelabeling: whitelabelingRouter,
|
||||||
schedule: scheduleRouter,
|
schedule: scheduleRouter,
|
||||||
rollback: rollbackRouter,
|
rollback: rollbackRouter,
|
||||||
volumeBackups: volumeBackupsRouter,
|
volumeBackups: volumeBackupsRouter,
|
||||||
|
|||||||
106
apps/dokploy/server/api/routers/proprietary/whitelabeling.ts
Normal file
106
apps/dokploy/server/api/routers/proprietary/whitelabeling.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import {
|
||||||
|
getWebServerSettings,
|
||||||
|
IS_CLOUD,
|
||||||
|
updateWebServerSettings,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { apiUpdateWhitelabeling } from "@/server/db/schema";
|
||||||
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
enterpriseProcedure,
|
||||||
|
protectedProcedure,
|
||||||
|
publicProcedure,
|
||||||
|
} from "../../trpc";
|
||||||
|
|
||||||
|
export const whitelabelingRouter = createTRPCRouter({
|
||||||
|
get: protectedProcedure.query(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const settings = await getWebServerSettings();
|
||||||
|
return settings?.whitelabelingConfig ?? null;
|
||||||
|
}),
|
||||||
|
|
||||||
|
update: enterpriseProcedure
|
||||||
|
.input(apiUpdateWhitelabeling)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Whitelabeling is not available in Cloud",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.user.role !== "owner") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Only the owner can update whitelabeling settings",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateWebServerSettings({
|
||||||
|
whitelabelingConfig: input.whitelabelingConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
|
|
||||||
|
reset: enterpriseProcedure.mutation(async ({ ctx }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Whitelabeling is not available in Cloud",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.user.role !== "owner") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Only the owner can reset whitelabeling settings",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateWebServerSettings({
|
||||||
|
whitelabelingConfig: {
|
||||||
|
appName: null,
|
||||||
|
appDescription: null,
|
||||||
|
logoUrl: null,
|
||||||
|
faviconUrl: null,
|
||||||
|
customCss: null,
|
||||||
|
loginLogoUrl: null,
|
||||||
|
supportUrl: null,
|
||||||
|
docsUrl: null,
|
||||||
|
errorPageTitle: null,
|
||||||
|
errorPageDescription: null,
|
||||||
|
metaTitle: null,
|
||||||
|
footerText: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Public endpoint only for unauthenticated pages (login, register, error)
|
||||||
|
// Returns only the fields needed for public pages
|
||||||
|
getPublic: publicProcedure.query(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const settings = await getWebServerSettings();
|
||||||
|
const config = settings?.whitelabelingConfig;
|
||||||
|
if (!config) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
appName: config.appName,
|
||||||
|
appDescription: config.appDescription,
|
||||||
|
logoUrl: config.logoUrl,
|
||||||
|
loginLogoUrl: config.loginLogoUrl,
|
||||||
|
faviconUrl: config.faviconUrl,
|
||||||
|
customCss: config.customCss,
|
||||||
|
metaTitle: config.metaTitle,
|
||||||
|
errorPageTitle: config.errorPageTitle,
|
||||||
|
errorPageDescription: config.errorPageDescription,
|
||||||
|
footerText: config.footerText,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -101,7 +101,10 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return memberResult;
|
return memberResult;
|
||||||
}),
|
}),
|
||||||
session: protectedProcedure.query(async ({ ctx }) => {
|
session: publicProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!ctx.user || !ctx.session || !ctx.session.activeOrganizationId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: ctx.user.id,
|
id: ctx.user.id,
|
||||||
|
|||||||
25
apps/dokploy/utils/hooks/use-whitelabeling.ts
Normal file
25
apps/dokploy/utils/hooks/use-whitelabeling.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to access whitelabeling config for authenticated pages (dashboard, services, etc.).
|
||||||
|
* Requires the user to be logged in.
|
||||||
|
*/
|
||||||
|
export function useWhitelabeling() {
|
||||||
|
const { data, ...rest } = api.whitelabeling.get.useQuery(undefined, {
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
return { config: data ?? null, ...rest };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to access the public whitelabeling config.
|
||||||
|
* Only for unauthenticated pages (login, register, error, invitation, password reset).
|
||||||
|
*/
|
||||||
|
export function useWhitelabelingPublic() {
|
||||||
|
const { data, ...rest } = api.whitelabeling.getPublic.useQuery(undefined, {
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
return { config: data ?? null, ...rest };
|
||||||
|
}
|
||||||
@@ -2,22 +2,23 @@ import path from "node:path";
|
|||||||
import Docker from "dockerode";
|
import Docker from "dockerode";
|
||||||
|
|
||||||
export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
||||||
export const DOCKER_API_VERSION = process.env.DOCKER_API_VERSION;
|
export const DOKPLOY_DOCKER_API_VERSION =
|
||||||
export const DOCKER_HOST = process.env.DOCKER_HOST;
|
process.env.DOKPLOY_DOCKER_API_VERSION;
|
||||||
export const DOCKER_PORT = process.env.DOCKER_PORT
|
export const DOKPLOY_DOCKER_HOST = process.env.DOKPLOY_DOCKER_HOST;
|
||||||
? Number(process.env.DOCKER_PORT)
|
export const DOKPLOY_DOCKER_PORT = process.env.DOKPLOY_DOCKER_PORT
|
||||||
|
? Number(process.env.DOKPLOY_DOCKER_PORT)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
export const CLEANUP_CRON_JOB = "50 23 * * *";
|
export const CLEANUP_CRON_JOB = "50 23 * * *";
|
||||||
export const docker = new Docker({
|
export const docker = new Docker({
|
||||||
...(DOCKER_API_VERSION && {
|
...(DOKPLOY_DOCKER_API_VERSION && {
|
||||||
version: DOCKER_API_VERSION,
|
version: DOKPLOY_DOCKER_API_VERSION,
|
||||||
}),
|
}),
|
||||||
...(DOCKER_HOST && {
|
...(DOKPLOY_DOCKER_HOST && {
|
||||||
host: DOCKER_HOST,
|
host: DOKPLOY_DOCKER_HOST,
|
||||||
}),
|
}),
|
||||||
...(DOCKER_PORT && {
|
...(DOKPLOY_DOCKER_PORT && {
|
||||||
port: DOCKER_PORT,
|
port: DOKPLOY_DOCKER_PORT,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,36 @@ export const webServerSettings = pgTable("webServerSettings", {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
// Whitelabeling Configuration (Enterprise / Proprietary)
|
||||||
|
whitelabelingConfig: jsonb("whitelabelingConfig")
|
||||||
|
.$type<{
|
||||||
|
appName: string | null;
|
||||||
|
appDescription: string | null;
|
||||||
|
logoUrl: string | null;
|
||||||
|
faviconUrl: string | null;
|
||||||
|
customCss: string | null;
|
||||||
|
loginLogoUrl: string | null;
|
||||||
|
supportUrl: string | null;
|
||||||
|
docsUrl: string | null;
|
||||||
|
errorPageTitle: string | null;
|
||||||
|
errorPageDescription: string | null;
|
||||||
|
metaTitle: string | null;
|
||||||
|
footerText: string | null;
|
||||||
|
}>()
|
||||||
|
.default({
|
||||||
|
appName: null,
|
||||||
|
appDescription: null,
|
||||||
|
logoUrl: null,
|
||||||
|
faviconUrl: null,
|
||||||
|
customCss: null,
|
||||||
|
loginLogoUrl: null,
|
||||||
|
supportUrl: null,
|
||||||
|
docsUrl: null,
|
||||||
|
errorPageTitle: null,
|
||||||
|
errorPageDescription: null,
|
||||||
|
metaTitle: null,
|
||||||
|
footerText: null,
|
||||||
|
}),
|
||||||
// Cache Cleanup Configuration
|
// Cache Cleanup Configuration
|
||||||
cleanupCacheApplications: boolean("cleanupCacheApplications")
|
cleanupCacheApplications: boolean("cleanupCacheApplications")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -154,6 +184,33 @@ export const apiUpdateDockerCleanup = z.object({
|
|||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Whitelabeling validation schemas
|
||||||
|
const safeUrl = z
|
||||||
|
.string()
|
||||||
|
.refine((url) => /^https?:\/\//i.test(url), {
|
||||||
|
message: "Only http:// and https:// URLs are allowed",
|
||||||
|
})
|
||||||
|
.nullable();
|
||||||
|
|
||||||
|
export const whitelabelingConfigSchema = z.object({
|
||||||
|
appName: z.string().nullable(),
|
||||||
|
appDescription: z.string().nullable(),
|
||||||
|
logoUrl: safeUrl,
|
||||||
|
faviconUrl: safeUrl,
|
||||||
|
customCss: z.string().nullable(),
|
||||||
|
loginLogoUrl: safeUrl,
|
||||||
|
supportUrl: safeUrl,
|
||||||
|
docsUrl: safeUrl,
|
||||||
|
errorPageTitle: z.string().nullable(),
|
||||||
|
errorPageDescription: z.string().nullable(),
|
||||||
|
metaTitle: z.string().nullable(),
|
||||||
|
footerText: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiUpdateWhitelabeling = z.object({
|
||||||
|
whitelabelingConfig: whitelabelingConfigSchema,
|
||||||
|
});
|
||||||
|
|
||||||
export const apiUpdateWebServerMonitoring = z.object({
|
export const apiUpdateWebServerMonitoring = z.object({
|
||||||
metricsConfig: z
|
metricsConfig: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { sendEmailNotification } from "../utils/notifications/utils";
|
||||||
sendDiscordNotification,
|
|
||||||
sendEmailNotification,
|
|
||||||
} from "../utils/notifications/utils";
|
|
||||||
export const sendEmail = async ({
|
export const sendEmail = async ({
|
||||||
email,
|
email,
|
||||||
subject,
|
subject,
|
||||||
@@ -26,26 +23,3 @@ export const sendEmail = async ({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendDiscordNotificationWelcome = async (email: string) => {
|
|
||||||
await sendDiscordNotification(
|
|
||||||
{
|
|
||||||
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "New User Registered",
|
|
||||||
color: 0x00ff00,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: "Email",
|
|
||||||
value: email,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
timestamp: new Date(),
|
|
||||||
footer: {
|
|
||||||
text: "Dokploy User Registration Notification",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@@ -119,6 +119,9 @@ importers:
|
|||||||
'@codemirror/autocomplete':
|
'@codemirror/autocomplete':
|
||||||
specifier: ^6.18.6
|
specifier: ^6.18.6
|
||||||
version: 6.20.0
|
version: 6.20.0
|
||||||
|
'@codemirror/lang-css':
|
||||||
|
specifier: ^6.3.1
|
||||||
|
version: 6.3.1
|
||||||
'@codemirror/lang-json':
|
'@codemirror/lang-json':
|
||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.2
|
version: 6.0.2
|
||||||
@@ -1264,6 +1267,9 @@ packages:
|
|||||||
'@codemirror/commands@6.10.2':
|
'@codemirror/commands@6.10.2':
|
||||||
resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==}
|
resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==}
|
||||||
|
|
||||||
|
'@codemirror/lang-css@6.3.1':
|
||||||
|
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
|
||||||
|
|
||||||
'@codemirror/lang-json@6.0.2':
|
'@codemirror/lang-json@6.0.2':
|
||||||
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
|
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
|
||||||
|
|
||||||
@@ -1816,6 +1822,9 @@ packages:
|
|||||||
'@lezer/common@1.5.1':
|
'@lezer/common@1.5.1':
|
||||||
resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==}
|
resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==}
|
||||||
|
|
||||||
|
'@lezer/css@1.3.1':
|
||||||
|
resolution: {integrity: sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==}
|
||||||
|
|
||||||
'@lezer/highlight@1.2.3':
|
'@lezer/highlight@1.2.3':
|
||||||
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
|
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
|
||||||
|
|
||||||
@@ -8803,6 +8812,14 @@ snapshots:
|
|||||||
'@codemirror/view': 6.39.15
|
'@codemirror/view': 6.39.15
|
||||||
'@lezer/common': 1.5.1
|
'@lezer/common': 1.5.1
|
||||||
|
|
||||||
|
'@codemirror/lang-css@6.3.1':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.20.0
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@lezer/common': 1.5.1
|
||||||
|
'@lezer/css': 1.3.1
|
||||||
|
|
||||||
'@codemirror/lang-json@6.0.2':
|
'@codemirror/lang-json@6.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/language': 6.12.1
|
'@codemirror/language': 6.12.1
|
||||||
@@ -9294,6 +9311,12 @@ snapshots:
|
|||||||
|
|
||||||
'@lezer/common@1.5.1': {}
|
'@lezer/common@1.5.1': {}
|
||||||
|
|
||||||
|
'@lezer/css@1.3.1':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.1
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
'@lezer/highlight@1.2.3':
|
'@lezer/highlight@1.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lezer/common': 1.5.1
|
'@lezer/common': 1.5.1
|
||||||
|
|||||||
Reference in New Issue
Block a user