mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 05:11:26 +00:00
Merge pull request #5649 from imrja8/feat/dual-mode-branding
feat(enterprise): dual-mode branding with separate light/dark theme logos
This commit is contained in:
@@ -62,6 +62,10 @@ const baseSettings: WebServerSettings = {
|
||||
errorPageDescription: null,
|
||||
ogImageUrl: null,
|
||||
footerText: null,
|
||||
logoLightUrl: null,
|
||||
loginLogoLightUrl: null,
|
||||
faviconLightUrl: null,
|
||||
dualModeBranding: false,
|
||||
},
|
||||
cleanupCacheApplications: false,
|
||||
cleanupCacheOnCompose: false,
|
||||
|
||||
@@ -186,6 +186,7 @@ export const ImpersonationBar = () => {
|
||||
<Logo
|
||||
className="w-10 h-10"
|
||||
logoUrl={whitelabeling?.logoUrl || undefined}
|
||||
logoLightUrl={whitelabeling?.logoLightUrl || undefined}
|
||||
/>
|
||||
{!isImpersonating ? (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
|
||||
@@ -19,6 +19,10 @@ export const OnboardingLayout = ({ children, leftPanel }: Props) => {
|
||||
"\u201CThe Open Source alternative to Netlify, Vercel, Heroku.\u201D";
|
||||
const logoUrl =
|
||||
whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined;
|
||||
const logoLightUrl =
|
||||
whitelabeling?.loginLogoLightUrl ||
|
||||
whitelabeling?.logoLightUrl ||
|
||||
undefined;
|
||||
|
||||
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">
|
||||
@@ -28,7 +32,11 @@ export const OnboardingLayout = ({ children, leftPanel }: Props) => {
|
||||
href="/"
|
||||
className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary"
|
||||
>
|
||||
<Logo className="size-10" logoUrl={logoUrl} />
|
||||
<Logo
|
||||
className="size-10"
|
||||
logoUrl={logoUrl}
|
||||
logoLightUrl={logoLightUrl}
|
||||
/>
|
||||
{appName}
|
||||
</Link>
|
||||
<div className="relative z-20 mt-auto">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -12,12 +13,19 @@ interface WhitelabelingPreviewProps {
|
||||
config: {
|
||||
appName?: string;
|
||||
logoUrl?: string;
|
||||
logoLightUrl?: string;
|
||||
dualModeBranding?: boolean;
|
||||
footerText?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function WhitelabelingPreview({ config }: WhitelabelingPreviewProps) {
|
||||
const appName = config.appName || "Dokploy";
|
||||
const { resolvedTheme } = useTheme();
|
||||
const effectiveLogoUrl =
|
||||
config.dualModeBranding && resolvedTheme === "light" && config.logoLightUrl
|
||||
? config.logoLightUrl
|
||||
: config.logoUrl;
|
||||
|
||||
return (
|
||||
<Card className="bg-transparent">
|
||||
@@ -31,9 +39,9 @@ export function WhitelabelingPreview({ config }: WhitelabelingPreviewProps) {
|
||||
<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 ? (
|
||||
{effectiveLogoUrl ? (
|
||||
<img
|
||||
src={config.logoUrl}
|
||||
src={effectiveLogoUrl}
|
||||
alt="Preview Logo"
|
||||
className="size-8 rounded-sm object-contain"
|
||||
/>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { WhitelabelingPreview } from "./whitelabeling-preview";
|
||||
@@ -36,20 +37,42 @@ const safeUrlField = z
|
||||
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(),
|
||||
footerText: z.string(),
|
||||
ogImageUrl: safeUrlField,
|
||||
});
|
||||
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(),
|
||||
footerText: z.string(),
|
||||
ogImageUrl: safeUrlField,
|
||||
logoLightUrl: z.string(),
|
||||
loginLogoLightUrl: z.string(),
|
||||
faviconLightUrl: z.string(),
|
||||
dualModeBranding: z.boolean(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (!data.dualModeBranding) return;
|
||||
for (const field of [
|
||||
"logoLightUrl",
|
||||
"loginLogoLightUrl",
|
||||
"faviconLightUrl",
|
||||
] as const) {
|
||||
const val = data[field];
|
||||
if (val !== "" && !/^https?:\/\//i.test(val)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Only http:// and https:// URLs are allowed",
|
||||
path: [field],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type FormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -195,6 +218,10 @@ export function WhitelabelingSettings() {
|
||||
errorPageDescription: "",
|
||||
footerText: "",
|
||||
ogImageUrl: "",
|
||||
logoLightUrl: "",
|
||||
loginLogoLightUrl: "",
|
||||
faviconLightUrl: "",
|
||||
dualModeBranding: false,
|
||||
},
|
||||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
@@ -214,6 +241,10 @@ export function WhitelabelingSettings() {
|
||||
errorPageDescription: data.errorPageDescription ?? "",
|
||||
footerText: data.footerText ?? "",
|
||||
ogImageUrl: data.ogImageUrl ?? "",
|
||||
logoLightUrl: data.logoLightUrl ?? "",
|
||||
loginLogoLightUrl: data.loginLogoLightUrl ?? "",
|
||||
faviconLightUrl: data.faviconLightUrl ?? "",
|
||||
dualModeBranding: data.dualModeBranding ?? false,
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
@@ -244,6 +275,10 @@ export function WhitelabelingSettings() {
|
||||
errorPageDescription: values.errorPageDescription || null,
|
||||
footerText: values.footerText || null,
|
||||
ogImageUrl: values.ogImageUrl || null,
|
||||
logoLightUrl: values.logoLightUrl || null,
|
||||
loginLogoLightUrl: values.loginLogoLightUrl || null,
|
||||
faviconLightUrl: values.faviconLightUrl || null,
|
||||
dualModeBranding: values.dualModeBranding,
|
||||
},
|
||||
})
|
||||
.then(async () => {
|
||||
@@ -412,6 +447,111 @@ export function WhitelabelingSettings() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Light Mode Logos Section */}
|
||||
<Card className="bg-transparent">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<CardTitle>Light Mode Logos</CardTitle>
|
||||
<CardDescription>
|
||||
Use separate logos for light and dark themes. When enabled,
|
||||
the logos in Branding are used for dark mode.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dualModeBranding"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Switch
|
||||
aria-label="Enable light mode logos"
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
if (!checked) {
|
||||
form.clearErrors([
|
||||
"logoLightUrl",
|
||||
"loginLogoLightUrl",
|
||||
"faviconLightUrl",
|
||||
]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div
|
||||
className="grid transition-all duration-300 ease-in-out"
|
||||
style={{
|
||||
gridTemplateRows: form.watch("dualModeBranding")
|
||||
? "1fr"
|
||||
: "0fr",
|
||||
}}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="logoLightUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Logo URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://example.com/logo-light.svg"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="loginLogoLightUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Login Page Logo URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://example.com/login-logo-light.svg"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="faviconLightUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Favicon URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://example.com/favicon-light.ico"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Follows the browser/OS theme, not the website theme.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Appearance Section */}
|
||||
<Card className="bg-transparent">
|
||||
<CardHeader>
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
logoUrl?: string;
|
||||
logoLightUrl?: string;
|
||||
}
|
||||
|
||||
export const Logo = ({ className = "size-14", logoUrl }: Props) => {
|
||||
if (logoUrl) {
|
||||
export const Logo = ({
|
||||
className = "size-14",
|
||||
logoUrl,
|
||||
logoLightUrl,
|
||||
}: Props) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const effectiveUrl =
|
||||
resolvedTheme === "light" && logoLightUrl ? logoLightUrl : logoUrl;
|
||||
|
||||
if (effectiveUrl) {
|
||||
return (
|
||||
// biome-ignore lint/performance/noImgElement: this is for dynamic logo loading
|
||||
<img
|
||||
src={logoUrl}
|
||||
src={effectiveUrl}
|
||||
alt="Organization Logo"
|
||||
className={cn(className, "object-contain rounded-sm")}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ interface WhitelabelingDocumentProps {
|
||||
appDescription: string | null;
|
||||
ogImageUrl: string | null;
|
||||
faviconHref: string | null;
|
||||
faviconLightHref: string | null;
|
||||
customCss: string | null;
|
||||
baseUrl: string;
|
||||
}
|
||||
@@ -22,6 +23,7 @@ export default function Document({
|
||||
appDescription,
|
||||
ogImageUrl,
|
||||
faviconHref,
|
||||
faviconLightHref,
|
||||
customCss,
|
||||
baseUrl,
|
||||
}: WhitelabelingDocumentProps) {
|
||||
@@ -44,7 +46,22 @@ export default function Document({
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<link rel="icon" href={faviconHref || "/icon.svg"} />
|
||||
{faviconLightHref ? (
|
||||
<>
|
||||
<link
|
||||
rel="icon"
|
||||
href={faviconHref || "/icon.svg"}
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
href={faviconLightHref}
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<link rel="icon" href={faviconHref || "/icon.svg"} />
|
||||
)}
|
||||
{customCss && (
|
||||
<style
|
||||
id="whitelabeling-styles"
|
||||
@@ -69,6 +86,7 @@ declare global {
|
||||
appDescription: string | null;
|
||||
ogImageUrl: string | null;
|
||||
faviconHref: string | null;
|
||||
faviconLightHref: string | null;
|
||||
customCss: string | null;
|
||||
};
|
||||
expiresAt: number;
|
||||
@@ -84,6 +102,7 @@ Document.getInitialProps = async (
|
||||
let appDescription: string | null = null;
|
||||
let ogImageUrl: string | null = null;
|
||||
let faviconHref: string | null = null;
|
||||
let faviconLightHref: string | null = null;
|
||||
let customCss: string | null = null;
|
||||
|
||||
const host = ctx.req?.headers?.host || "localhost:3000";
|
||||
@@ -113,6 +132,7 @@ Document.getInitialProps = async (
|
||||
? config.customCss.replace(/<\/\s*style[^>]*>/gi, "")
|
||||
: null;
|
||||
faviconHref = config.faviconUrl || null;
|
||||
faviconLightHref = config.faviconLightUrl || null;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to defaults if settings can't be read (e.g. DB not ready)
|
||||
@@ -124,6 +144,7 @@ Document.getInitialProps = async (
|
||||
appDescription,
|
||||
ogImageUrl,
|
||||
faviconHref,
|
||||
faviconLightHref,
|
||||
customCss,
|
||||
},
|
||||
expiresAt: Date.now() + SETTINGS_CACHE_TTL,
|
||||
@@ -135,6 +156,7 @@ Document.getInitialProps = async (
|
||||
appDescription,
|
||||
ogImageUrl,
|
||||
faviconHref,
|
||||
faviconLightHref,
|
||||
customCss,
|
||||
baseUrl,
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ export default function ErrorPage({ statusCode }: Props) {
|
||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||
const appName = whitelabeling?.appName || "Dokploy";
|
||||
const logoUrl = whitelabeling?.logoUrl || undefined;
|
||||
const logoLightUrl = whitelabeling?.logoLightUrl || undefined;
|
||||
const errorTitle = whitelabeling?.errorPageTitle;
|
||||
const errorDescription = whitelabeling?.errorPageDescription;
|
||||
|
||||
@@ -22,7 +23,7 @@ export default function ErrorPage({ statusCode }: Props) {
|
||||
<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">
|
||||
<Link href="/" className="flex flex-row items-center gap-2">
|
||||
<Logo logoUrl={logoUrl} />
|
||||
<Logo logoUrl={logoUrl} logoLightUrl={logoLightUrl} />
|
||||
<span className="font-medium text-sm">{appName}</span>
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { generateServerSideHelper } from "@/utils/create-server-helpers";
|
||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||
import { Fingerprint } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
@@ -49,6 +48,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { generateServerSideHelper } from "@/utils/create-server-helpers";
|
||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||
|
||||
const LoginSchema = z.object({
|
||||
@@ -306,6 +306,11 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) {
|
||||
whitelabeling?.logoUrl ||
|
||||
undefined
|
||||
}
|
||||
logoLightUrl={
|
||||
whitelabeling?.loginLogoLightUrl ||
|
||||
whitelabeling?.logoLightUrl ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
Sign in
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getUserByToken, IS_CLOUD } from "@dokploy/server";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { generateServerSideHelper } from "@/utils/create-server-helpers";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -26,6 +25,7 @@ import { pushToDataLayer } from "@/lib/analytics";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { generateServerSideHelper } from "@/utils/create-server-helpers";
|
||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||
|
||||
const registerSchema = z
|
||||
@@ -164,6 +164,11 @@ const Invitation = ({
|
||||
whitelabeling?.logoUrl ||
|
||||
undefined
|
||||
}
|
||||
logoLightUrl={
|
||||
whitelabeling?.loginLogoLightUrl ||
|
||||
whitelabeling?.logoLightUrl ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
Invitation
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { generateServerSideHelper } from "@/utils/create-server-helpers";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
@@ -29,6 +28,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { pushToDataLayer } from "@/lib/analytics";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { generateServerSideHelper } from "@/utils/create-server-helpers";
|
||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||
|
||||
const registerSchema = z
|
||||
@@ -138,6 +138,11 @@ const Register = ({ isCloud }: Props) => {
|
||||
whitelabeling?.logoUrl ||
|
||||
undefined
|
||||
}
|
||||
logoLightUrl={
|
||||
whitelabeling?.loginLogoLightUrl ||
|
||||
whitelabeling?.logoLightUrl ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
{isCloud ? "Sign Up" : "Setup the server"}
|
||||
|
||||
@@ -106,6 +106,11 @@ export default function Home({ tokenResetPassword }: Props) {
|
||||
whitelabeling?.logoUrl ||
|
||||
undefined
|
||||
}
|
||||
logoLightUrl={
|
||||
whitelabeling?.loginLogoLightUrl ||
|
||||
whitelabeling?.logoLightUrl ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
Reset Password
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IS_CLOUD } from "@dokploy/server";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { generateServerSideHelper } from "@/utils/create-server-helpers";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { generateServerSideHelper } from "@/utils/create-server-helpers";
|
||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||
|
||||
const loginSchema = z.object({
|
||||
@@ -92,6 +92,11 @@ export default function Home() {
|
||||
logoUrl={
|
||||
whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined
|
||||
}
|
||||
logoLightUrl={
|
||||
whitelabeling?.loginLogoLightUrl ||
|
||||
whitelabeling?.logoLightUrl ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
<span className="font-medium text-sm">
|
||||
{whitelabeling?.appName || "Dokploy"}
|
||||
|
||||
@@ -87,6 +87,10 @@ export const whitelabelingRouter = createTRPCRouter({
|
||||
errorPageDescription: null,
|
||||
ogImageUrl: null,
|
||||
footerText: null,
|
||||
logoLightUrl: null,
|
||||
loginLogoLightUrl: null,
|
||||
faviconLightUrl: null,
|
||||
dualModeBranding: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -88,6 +88,10 @@ export const webServerSettings = pgTable("webServerSettings", {
|
||||
errorPageDescription: string | null;
|
||||
footerText: string | null;
|
||||
ogImageUrl: string | null;
|
||||
logoLightUrl: string | null;
|
||||
loginLogoLightUrl: string | null;
|
||||
faviconLightUrl: string | null;
|
||||
dualModeBranding: boolean;
|
||||
}>()
|
||||
.default({
|
||||
appName: null,
|
||||
@@ -102,6 +106,10 @@ export const webServerSettings = pgTable("webServerSettings", {
|
||||
errorPageDescription: null,
|
||||
footerText: null,
|
||||
ogImageUrl: null,
|
||||
logoLightUrl: null,
|
||||
loginLogoLightUrl: null,
|
||||
faviconLightUrl: null,
|
||||
dualModeBranding: false,
|
||||
}),
|
||||
// Deployment Configuration (self-hosted only)
|
||||
remoteServersOnly: boolean("remoteServersOnly").notNull().default(false),
|
||||
@@ -225,6 +233,10 @@ export const whitelabelingConfigSchema = z.object({
|
||||
errorPageDescription: z.string().nullable(),
|
||||
footerText: z.string().nullable(),
|
||||
ogImageUrl: safeUrl,
|
||||
logoLightUrl: safeUrl,
|
||||
loginLogoLightUrl: safeUrl,
|
||||
faviconLightUrl: safeUrl,
|
||||
dualModeBranding: z.boolean(),
|
||||
});
|
||||
|
||||
export const apiUpdateWhitelabeling = z.object({
|
||||
|
||||
@@ -14,6 +14,10 @@ export interface PublicWhitelabelingConfig {
|
||||
errorPageTitle: string | null;
|
||||
errorPageDescription: string | null;
|
||||
footerText: string | null;
|
||||
logoLightUrl: string | null;
|
||||
loginLogoLightUrl: string | null;
|
||||
faviconLightUrl: string | null;
|
||||
dualModeBranding: boolean;
|
||||
}
|
||||
|
||||
// Self-hosted is single-tenant; gate on the oldest organization's license,
|
||||
@@ -54,5 +58,15 @@ export const getPublicWhitelabelingConfig =
|
||||
errorPageTitle: config.errorPageTitle,
|
||||
errorPageDescription: config.errorPageDescription,
|
||||
footerText: config.footerText,
|
||||
dualModeBranding: config.dualModeBranding ?? false,
|
||||
logoLightUrl: config.dualModeBranding
|
||||
? (config.logoLightUrl ?? null)
|
||||
: null,
|
||||
loginLogoLightUrl: config.dualModeBranding
|
||||
? (config.loginLogoLightUrl ?? null)
|
||||
: null,
|
||||
faviconLightUrl: config.dualModeBranding
|
||||
? (config.faviconLightUrl ?? null)
|
||||
: null,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user