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 { 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";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
export const OnboardingLayout = ({ children }: Props) => {
|
export const OnboardingLayout = ({ children }: Props) => {
|
||||||
|
const { data: whitelabel } = api.settings.getWhitelabelSettings.useQuery();
|
||||||
|
const appName = whitelabel?.whitelabelAppName ?? "Dokploy";
|
||||||
|
const logoUrl =
|
||||||
|
whitelabel?.whitelabelLogoUrl ?? whitelabel?.whitelabelLoginLogoUrl;
|
||||||
|
|
||||||
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" />
|
||||||
|
{whitelabel?.whitelabelLoginBackgroundImageUrl && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-30"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${whitelabel.whitelabelLoginBackgroundImageUrl})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Link
|
<Link
|
||||||
href="https://dokploy.com"
|
href="https://dokploy.com"
|
||||||
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 ?? undefined} />
|
||||||
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">
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import {
|
|||||||
Key,
|
Key,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
LogIn,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Package,
|
Package,
|
||||||
|
Palette,
|
||||||
PieChart,
|
PieChart,
|
||||||
Server,
|
Server,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
@@ -30,7 +32,6 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
LogIn,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
@@ -416,6 +417,15 @@ 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/whitelabelling",
|
||||||
|
icon: Palette,
|
||||||
|
// Enterprise only – page shows gate if no license
|
||||||
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
help: [
|
help: [
|
||||||
@@ -546,6 +556,7 @@ function SidebarLogo() {
|
|||||||
refetch,
|
refetch,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = api.organization.all.useQuery();
|
} = api.organization.all.useQuery();
|
||||||
|
const { data: whitelabel } = api.settings.getWhitelabelSettings.useQuery();
|
||||||
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
|
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
|
||||||
api.organization.delete.useMutation();
|
api.organization.delete.useMutation();
|
||||||
const { mutateAsync: setDefaultOrganization, isLoading: isSettingDefault } =
|
const { mutateAsync: setDefaultOrganization, isLoading: isSettingDefault } =
|
||||||
@@ -611,7 +622,11 @@ function SidebarLogo() {
|
|||||||
"transition-all",
|
"transition-all",
|
||||||
state === "collapsed" ? "size-4" : "size-5",
|
state === "collapsed" ? "size-4" : "size-5",
|
||||||
)}
|
)}
|
||||||
logoUrl={activeOrganization?.logo || undefined}
|
logoUrl={
|
||||||
|
activeOrganization?.logo ||
|
||||||
|
whitelabel?.whitelabelLogoUrl ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -621,7 +636,9 @@ function SidebarLogo() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium leading-none">
|
<p className="text-sm font-medium leading-none">
|
||||||
{activeOrganization?.name ?? "Select Organization"}
|
{activeOrganization?.name ??
|
||||||
|
whitelabel?.whitelabelAppName ??
|
||||||
|
"Select Organization"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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) {
|
export default function Home({ IS_CLOUD }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
|
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
|
||||||
|
const { data: whitelabel } = api.settings.getWhitelabelSettings.useQuery();
|
||||||
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||||
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
||||||
const [isBackupCodeLoading, setIsBackupCodeLoading] = 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<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
|
||||||
Sign in
|
className="size-12"
|
||||||
|
logoUrl={loginLogoUrl ?? undefined}
|
||||||
|
/>
|
||||||
|
{loginTitle}
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Enter your email and password to sign in
|
{loginSubtitle}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import {
|
|||||||
apiServerSchema,
|
apiServerSchema,
|
||||||
apiTraefikConfig,
|
apiTraefikConfig,
|
||||||
apiUpdateDockerCleanup,
|
apiUpdateDockerCleanup,
|
||||||
|
apiUpdateWhitelabel,
|
||||||
projects,
|
projects,
|
||||||
server,
|
server,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
@@ -72,6 +73,7 @@ import { appRouter } from "../root";
|
|||||||
import {
|
import {
|
||||||
adminProcedure,
|
adminProcedure,
|
||||||
createTRPCRouter,
|
createTRPCRouter,
|
||||||
|
enterpriseProcedure,
|
||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
} from "../trpc";
|
} from "../trpc";
|
||||||
@@ -84,6 +86,57 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
const settings = await getWebServerSettings();
|
const settings = await getWebServerSettings();
|
||||||
return settings;
|
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 () => {
|
reloadServer: adminProcedure.mutation(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ export const webServerSettings = pgTable("webServerSettings", {
|
|||||||
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.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(),
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
@@ -125,6 +133,18 @@ export const apiUpdateWebServerSettings = createSchema.partial().extend({
|
|||||||
cleanupCacheApplications: z.boolean().optional(),
|
cleanupCacheApplications: z.boolean().optional(),
|
||||||
cleanupCacheOnPreviews: z.boolean().optional(),
|
cleanupCacheOnPreviews: z.boolean().optional(),
|
||||||
cleanupCacheOnCompose: 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
|
export const apiAssignDomain = z
|
||||||
@@ -154,6 +174,21 @@ export const apiUpdateDockerCleanup = z.object({
|
|||||||
serverId: z.string().optional(),
|
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({
|
export const apiUpdateWebServerMonitoring = z.object({
|
||||||
metricsConfig: z
|
metricsConfig: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user