mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
feat(whitelabel): implement whitelabeling features and settings
- Added whitelabeling support, allowing customization of application name, logos, and login page. - Introduced new WhitelabelSettings component for managing whitelabel configurations. - Updated onboarding and sidebar layouts to reflect whitelabel settings dynamically. - Created database schema changes to accommodate new whitelabel fields. - Implemented API endpoints for retrieving and updating whitelabel settings.
This commit is contained in:
@@ -4,21 +4,35 @@ import { cn } from "@/lib/utils";
|
||||
import { GithubIcon } from "../icons/data-tools-icons";
|
||||
import { Logo } from "../shared/logo";
|
||||
import { Button } from "../ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export const OnboardingLayout = ({ children }: Props) => {
|
||||
const { data: whitelabel } = api.settings.getWhitelabelSettings.useQuery();
|
||||
const appName = whitelabel?.whitelabelAppName ?? "Dokploy";
|
||||
const logoUrl =
|
||||
whitelabel?.whitelabelLogoUrl ?? whitelabel?.whitelabelLoginLogoUrl;
|
||||
|
||||
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="relative hidden h-full flex-col p-10 text-primary dark:border-r lg:flex">
|
||||
<div className="absolute inset-0 bg-muted" />
|
||||
{whitelabel?.whitelabelLoginBackgroundImageUrl && (
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-30"
|
||||
style={{
|
||||
backgroundImage: `url(${whitelabel.whitelabelLoginBackgroundImageUrl})`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Link
|
||||
href="https://dokploy.com"
|
||||
className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary"
|
||||
>
|
||||
<Logo className="size-10" />
|
||||
Dokploy
|
||||
<Logo className="size-10" logoUrl={logoUrl ?? undefined} />
|
||||
{appName}
|
||||
</Link>
|
||||
<div className="relative z-20 mt-auto">
|
||||
<blockquote className="space-y-2">
|
||||
|
||||
@@ -21,8 +21,10 @@ import {
|
||||
Key,
|
||||
KeyRound,
|
||||
Loader2,
|
||||
LogIn,
|
||||
type LucideIcon,
|
||||
Package,
|
||||
Palette,
|
||||
PieChart,
|
||||
Server,
|
||||
ShieldCheck,
|
||||
@@ -30,7 +32,6 @@ import {
|
||||
Trash2,
|
||||
User,
|
||||
Users,
|
||||
LogIn,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
@@ -416,6 +417,15 @@ const MENU: Menu = {
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Whitelabeling",
|
||||
url: "/dashboard/settings/whitelabelling",
|
||||
icon: Palette,
|
||||
// Enterprise only – page shows gate if no license
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
},
|
||||
],
|
||||
|
||||
help: [
|
||||
@@ -546,6 +556,7 @@ function SidebarLogo() {
|
||||
refetch,
|
||||
isLoading,
|
||||
} = api.organization.all.useQuery();
|
||||
const { data: whitelabel } = api.settings.getWhitelabelSettings.useQuery();
|
||||
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
|
||||
api.organization.delete.useMutation();
|
||||
const { mutateAsync: setDefaultOrganization, isLoading: isSettingDefault } =
|
||||
@@ -611,7 +622,11 @@ function SidebarLogo() {
|
||||
"transition-all",
|
||||
state === "collapsed" ? "size-4" : "size-5",
|
||||
)}
|
||||
logoUrl={activeOrganization?.logo || undefined}
|
||||
logoUrl={
|
||||
activeOrganization?.logo ||
|
||||
whitelabel?.whitelabelLogoUrl ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -621,7 +636,9 @@ function SidebarLogo() {
|
||||
)}
|
||||
>
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{activeOrganization?.name ?? "Select Organization"}
|
||||
{activeOrganization?.name ??
|
||||
whitelabel?.whitelabelAppName ??
|
||||
"Select Organization"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2, Palette } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardDescription, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const whitelabelSchema = z.object({
|
||||
whitelabelAppName: z.string().min(1).max(100),
|
||||
whitelabelLogoUrl: z.union([z.string().url(), z.literal("")]).optional(),
|
||||
whitelabelLoginLogoUrl: z.union([z.string().url(), z.literal("")]).optional(),
|
||||
whitelabelFaviconUrl: z.union([z.string().url(), z.literal("")]).optional(),
|
||||
whitelabelLoginTitle: z.string().max(200).optional(),
|
||||
whitelabelLoginSubtitle: z.string().max(500).optional(),
|
||||
whitelabelLoginBackgroundImageUrl: z
|
||||
.union([z.string().url(), z.literal("")])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
type WhitelabelFormValues = z.infer<typeof whitelabelSchema>;
|
||||
|
||||
export function WhitelabelSettings() {
|
||||
const { data: settings, isLoading } =
|
||||
api.settings.getWebServerSettings.useQuery();
|
||||
const { mutateAsync: updateWhitelabel, isLoading: isSaving } =
|
||||
api.settings.updateWhitelabelSettings.useMutation();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const form = useForm<WhitelabelFormValues>({
|
||||
resolver: zodResolver(whitelabelSchema),
|
||||
defaultValues: {
|
||||
whitelabelAppName: "Dokploy",
|
||||
whitelabelLogoUrl: "",
|
||||
whitelabelLoginLogoUrl: "",
|
||||
whitelabelFaviconUrl: "",
|
||||
whitelabelLoginTitle: "",
|
||||
whitelabelLoginSubtitle: "",
|
||||
whitelabelLoginBackgroundImageUrl: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
form.reset({
|
||||
whitelabelAppName: settings.whitelabelAppName ?? "Dokploy",
|
||||
whitelabelLogoUrl: settings.whitelabelLogoUrl ?? "",
|
||||
whitelabelLoginLogoUrl: settings.whitelabelLoginLogoUrl ?? "",
|
||||
whitelabelFaviconUrl: settings.whitelabelFaviconUrl ?? "",
|
||||
whitelabelLoginTitle: settings.whitelabelLoginTitle ?? "",
|
||||
whitelabelLoginSubtitle: settings.whitelabelLoginSubtitle ?? "",
|
||||
whitelabelLoginBackgroundImageUrl:
|
||||
settings.whitelabelLoginBackgroundImageUrl ?? "",
|
||||
});
|
||||
}
|
||||
}, [settings, form]);
|
||||
|
||||
const onSubmit = async (values: WhitelabelFormValues) => {
|
||||
try {
|
||||
await updateWhitelabel({
|
||||
whitelabelAppName: values.whitelabelAppName || null,
|
||||
whitelabelLogoUrl: values.whitelabelLogoUrl || undefined,
|
||||
whitelabelLoginLogoUrl: values.whitelabelLoginLogoUrl || undefined,
|
||||
whitelabelFaviconUrl: values.whitelabelFaviconUrl || undefined,
|
||||
whitelabelLoginTitle: values.whitelabelLoginTitle || null,
|
||||
whitelabelLoginSubtitle: values.whitelabelLoginSubtitle || null,
|
||||
whitelabelLoginBackgroundImageUrl:
|
||||
values.whitelabelLoginBackgroundImageUrl || undefined,
|
||||
});
|
||||
toast.success("Whitelabel settings saved");
|
||||
utils.settings.getWebServerSettings.invalidate();
|
||||
utils.settings.getWhitelabelSettings.invalidate();
|
||||
} catch (e) {
|
||||
toast.error("Failed to save whitelabel settings");
|
||||
}
|
||||
};
|
||||
|
||||
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 whitelabel settings...
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg ">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="size-6 text-muted-foreground" />
|
||||
<CardTitle className="text-xl">Whitelabeling</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Customize the application name, logos, and login page for your brand.
|
||||
Leave URLs empty to use defaults.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-6"
|
||||
>
|
||||
<div className="space-y-4 pt-2 border-t">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Brand</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Application name and main logo (sidebar, header).
|
||||
</p>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="whitelabelAppName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Application name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Dokploy"
|
||||
{...field}
|
||||
className="max-w-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="whitelabelLogoUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Logo URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://example.com/logo.png"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="max-w-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Logo shown in the sidebar and header.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="whitelabelFaviconUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Favicon URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://example.com/favicon.ico"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="max-w-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pt-6 border-t">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Login page</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Customize the sign-in and registration screens.
|
||||
</p>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="whitelabelLoginLogoUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Login logo URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://example.com/login-logo.png"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="max-w-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Logo on the login and register pages. Falls back to the main
|
||||
logo if empty.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="whitelabelLoginTitle"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Login title</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Sign in"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="max-w-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="whitelabelLoginSubtitle"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Login subtitle</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter your email and password to sign in"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="max-w-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="whitelabelLoginBackgroundImageUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Login background image URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://example.com/background.jpg"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="max-w-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional background image for the login page.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4 border-t">
|
||||
<Button type="submit" disabled={isSaving}>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
"Save changes"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
84
apps/dokploy/pages/dashboard/settings/whitelabelling.tsx
Normal file
84
apps/dokploy/pages/dashboard/settings/whitelabelling.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
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 { WhitelabelSettings } from "@/components/proprietary/whitelabelling/whitelabel-settings";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||
|
||||
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 is part of Dokploy Enterprise. Add a valid license to customize logos, app name, and login page.",
|
||||
ctaLabel: "Go to License",
|
||||
}}
|
||||
>
|
||||
<WhitelabelSettings />
|
||||
</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 } = ctx;
|
||||
const locale = await getLocale(req.cookies);
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/settings/profile",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: ctx.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(),
|
||||
...(await serverSideTranslations(locale, ["settings"])),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -59,6 +59,7 @@ interface Props {
|
||||
export default function Home({ IS_CLOUD }: Props) {
|
||||
const router = useRouter();
|
||||
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
|
||||
const { data: whitelabel } = api.settings.getWhitelabelSettings.useQuery();
|
||||
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
||||
const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false);
|
||||
@@ -212,17 +213,27 @@ export default function Home({ IS_CLOUD }: Props) {
|
||||
</>
|
||||
);
|
||||
|
||||
const loginLogoUrl =
|
||||
whitelabel?.whitelabelLoginLogoUrl ?? whitelabel?.whitelabelLogoUrl;
|
||||
const loginTitle = whitelabel?.whitelabelLoginTitle ?? "Sign in";
|
||||
const loginSubtitle =
|
||||
whitelabel?.whitelabelLoginSubtitle ??
|
||||
"Enter your email and password to sign in";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
<Logo className="size-12" />
|
||||
Sign in
|
||||
<Logo
|
||||
className="size-12"
|
||||
logoUrl={loginLogoUrl ?? undefined}
|
||||
/>
|
||||
{loginTitle}
|
||||
</div>
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enter your email and password to sign in
|
||||
{loginSubtitle}
|
||||
</p>
|
||||
</div>
|
||||
{error && (
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
apiServerSchema,
|
||||
apiTraefikConfig,
|
||||
apiUpdateDockerCleanup,
|
||||
apiUpdateWhitelabel,
|
||||
projects,
|
||||
server,
|
||||
} from "@/server/db/schema";
|
||||
@@ -72,6 +73,7 @@ import { appRouter } from "../root";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
enterpriseProcedure,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "../trpc";
|
||||
@@ -84,6 +86,57 @@ export const settingsRouter = createTRPCRouter({
|
||||
const settings = await getWebServerSettings();
|
||||
return settings;
|
||||
}),
|
||||
getWhitelabelSettings: publicProcedure.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return null;
|
||||
}
|
||||
const settings = await getWebServerSettings();
|
||||
if (!settings) return null;
|
||||
return {
|
||||
whitelabelAppName: settings.whitelabelAppName ?? "Dokploy",
|
||||
whitelabelLogoUrl: settings.whitelabelLogoUrl ?? null,
|
||||
whitelabelLoginLogoUrl: settings.whitelabelLoginLogoUrl ?? null,
|
||||
whitelabelFaviconUrl: settings.whitelabelFaviconUrl ?? null,
|
||||
whitelabelLoginTitle: settings.whitelabelLoginTitle ?? null,
|
||||
whitelabelLoginSubtitle: settings.whitelabelLoginSubtitle ?? null,
|
||||
whitelabelLoginBackgroundImageUrl:
|
||||
settings.whitelabelLoginBackgroundImageUrl ?? null,
|
||||
};
|
||||
}),
|
||||
updateWhitelabelSettings: enterpriseProcedure
|
||||
.input(apiUpdateWhitelabel)
|
||||
.mutation(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
return null;
|
||||
}
|
||||
const updates: Record<string, unknown> = {};
|
||||
if (input.whitelabelAppName !== undefined)
|
||||
updates.whitelabelAppName = input.whitelabelAppName;
|
||||
if (input.whitelabelLogoUrl !== undefined)
|
||||
updates.whitelabelLogoUrl =
|
||||
input.whitelabelLogoUrl === "" ? null : input.whitelabelLogoUrl;
|
||||
if (input.whitelabelLoginLogoUrl !== undefined)
|
||||
updates.whitelabelLoginLogoUrl =
|
||||
input.whitelabelLoginLogoUrl === ""
|
||||
? null
|
||||
: input.whitelabelLoginLogoUrl;
|
||||
if (input.whitelabelFaviconUrl !== undefined)
|
||||
updates.whitelabelFaviconUrl =
|
||||
input.whitelabelFaviconUrl === ""
|
||||
? null
|
||||
: input.whitelabelFaviconUrl;
|
||||
if (input.whitelabelLoginTitle !== undefined)
|
||||
updates.whitelabelLoginTitle = input.whitelabelLoginTitle;
|
||||
if (input.whitelabelLoginSubtitle !== undefined)
|
||||
updates.whitelabelLoginSubtitle = input.whitelabelLoginSubtitle;
|
||||
if (input.whitelabelLoginBackgroundImageUrl !== undefined)
|
||||
updates.whitelabelLoginBackgroundImageUrl =
|
||||
input.whitelabelLoginBackgroundImageUrl === ""
|
||||
? null
|
||||
: input.whitelabelLoginBackgroundImageUrl;
|
||||
const updated = await updateWebServerSettings(updates as any);
|
||||
return updated;
|
||||
}),
|
||||
reloadServer: adminProcedure.mutation(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
|
||||
@@ -76,6 +76,14 @@ export const webServerSettings = pgTable("webServerSettings", {
|
||||
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
||||
.notNull()
|
||||
.default(false),
|
||||
// Whitelabel (Enterprise)
|
||||
whitelabelAppName: text("whitelabelAppName").default("Dokploy"),
|
||||
whitelabelLogoUrl: text("whitelabelLogoUrl"),
|
||||
whitelabelLoginLogoUrl: text("whitelabelLoginLogoUrl"),
|
||||
whitelabelFaviconUrl: text("whitelabelFaviconUrl"),
|
||||
whitelabelLoginTitle: text("whitelabelLoginTitle"),
|
||||
whitelabelLoginSubtitle: text("whitelabelLoginSubtitle"),
|
||||
whitelabelLoginBackgroundImageUrl: text("whitelabelLoginBackgroundImageUrl"),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
@@ -125,6 +133,18 @@ export const apiUpdateWebServerSettings = createSchema.partial().extend({
|
||||
cleanupCacheApplications: z.boolean().optional(),
|
||||
cleanupCacheOnPreviews: z.boolean().optional(),
|
||||
cleanupCacheOnCompose: z.boolean().optional(),
|
||||
whitelabelAppName: z.string().optional().nullable(),
|
||||
whitelabelLogoUrl: z.string().url().optional().nullable().or(z.literal("")),
|
||||
whitelabelLoginLogoUrl: z.string().url().optional().nullable().or(z.literal("")),
|
||||
whitelabelFaviconUrl: z.string().url().optional().nullable().or(z.literal("")),
|
||||
whitelabelLoginTitle: z.string().optional().nullable(),
|
||||
whitelabelLoginSubtitle: z.string().optional().nullable(),
|
||||
whitelabelLoginBackgroundImageUrl: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.nullable()
|
||||
.or(z.literal("")),
|
||||
});
|
||||
|
||||
export const apiAssignDomain = z
|
||||
@@ -154,6 +174,21 @@ export const apiUpdateDockerCleanup = z.object({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateWhitelabel = z.object({
|
||||
whitelabelAppName: z.string().min(1).max(100).optional().nullable(),
|
||||
whitelabelLogoUrl: z.string().url().optional().nullable().or(z.literal("")),
|
||||
whitelabelLoginLogoUrl: z.string().url().optional().nullable().or(z.literal("")),
|
||||
whitelabelFaviconUrl: z.string().url().optional().nullable().or(z.literal("")),
|
||||
whitelabelLoginTitle: z.string().max(200).optional().nullable(),
|
||||
whitelabelLoginSubtitle: z.string().max(500).optional().nullable(),
|
||||
whitelabelLoginBackgroundImageUrl: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.nullable()
|
||||
.or(z.literal("")),
|
||||
});
|
||||
|
||||
export const apiUpdateWebServerMonitoring = z.object({
|
||||
metricsConfig: z
|
||||
.object({
|
||||
|
||||
Reference in New Issue
Block a user