feat: add configurable headers and instance branding (#6690)

This commit is contained in:
Thomas authored and GitHub committed 2026-08-25 17:09:47 +02:00
1 parent 97d8390b20
commit 9303dbd284
102 files changed
+12153 -609

No files matched your search

+110
View File
@@ -1,5 +1,6 @@
import type { ColorScheme } from "@homarr/definitions";
import type { SupportedLanguage } from "@homarr/translation";
import { z } from "zod/v4";
export const defaultServerSettingsKeys = [
"analytics",
@@ -7,12 +8,120 @@ export const defaultServerSettingsKeys = [
"board",
"user",
"appearance",
"branding",
"culture",
"search",
] as const;
export type ServerSettingsRecord = Record<(typeof defaultServerSettingsKeys)[number], Record<string, unknown>>;
export const brandingRadiusOptions = ["xs", "sm", "md", "lg", "xl"] as const;
export type BrandingRadius = (typeof brandingRadiusOptions)[number];
export const authBrandingSchema = z.object({
showAppName: z.boolean(),
showLogo: z.boolean(),
showGreeting: z.boolean(),
});
const brandingImageUrlSchema = z
.string()
.trim()
.max(2_048)
.refine((value) => {
if (value.startsWith("/") && !value.startsWith("//")) return true;
try {
const url = new URL(value);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}, "Image must be an HTTP(S) URL or an app-relative path")
.nullable();
export const brandingServerSettingsSchema = z.object({
appName: z.string().trim().min(1).max(80),
greeting: z.string().trim().max(160),
logoImageUrl: brandingImageUrlSchema,
faviconImageUrl: brandingImageUrlSchema,
primaryColor: z.string().regex(/^#[\da-f]{6}$/iu, "Primary color must be a 6-digit hex color"),
secondaryColor: z.string().regex(/^#[\da-f]{6}$/iu, "Secondary color must be a 6-digit hex color"),
lockPrimaryColor: z.boolean(),
signInBackgroundImageUrl: brandingImageUrlSchema,
signInBackgroundOverlay: z.number().min(0).max(0.9),
authBranding: authBrandingSchema,
defaultRadius: z.enum(brandingRadiusOptions),
});
export type BrandingSettings = z.infer<typeof brandingServerSettingsSchema>;
export const defaultBrandingSettings: BrandingSettings = {
appName: "Homarr",
greeting: "",
logoImageUrl: null,
faviconImageUrl: null,
primaryColor: "#fa5252",
secondaryColor: "#fd7e14",
lockPrimaryColor: false,
signInBackgroundImageUrl: null,
signInBackgroundOverlay: 0.55,
authBranding: {
showAppName: true,
showLogo: true,
showGreeting: true,
},
defaultRadius: "md",
};
const hasDefaultBrandingColors = (branding: Pick<BrandingSettings, "primaryColor" | "secondaryColor">) =>
branding.primaryColor.toLowerCase() === defaultBrandingSettings.primaryColor &&
branding.secondaryColor.toLowerCase() === defaultBrandingSettings.secondaryColor;
export const getBrandingColorOverrides = (
branding: Pick<BrandingSettings, "primaryColor" | "secondaryColor">,
): Partial<Pick<BrandingSettings, "primaryColor" | "secondaryColor">> => {
if (hasDefaultBrandingColors(branding)) return {};
return {
primaryColor: branding.primaryColor,
secondaryColor: branding.secondaryColor,
};
};
const legacyLoginBrandingSchema = z.object({
showCustomAppNameOnLogin: z.boolean().optional(),
showCustomLogoOnLogin: z.boolean().optional(),
showCustomGreetingOnLogin: z.boolean().optional(),
showCustomAppNameOnInvite: z.boolean().optional(),
showCustomLogoOnInvite: z.boolean().optional(),
showCustomGreetingOnInvite: z.boolean().optional(),
});
export const parseBrandingSettings = (value: unknown): BrandingSettings => {
const partialValue = z.record(z.string(), z.unknown()).safeParse(value);
if (!partialValue.success) return defaultBrandingSettings;
const legacyBranding = legacyLoginBrandingSchema.safeParse(partialValue.data);
const partialAuthBranding = authBrandingSchema.partial().safeParse(partialValue.data.authBranding);
const authBranding = { ...defaultBrandingSettings.authBranding };
if (legacyBranding.success) {
const legacy = legacyBranding.data;
authBranding.showAppName = legacy.showCustomAppNameOnLogin ?? authBranding.showAppName;
authBranding.showLogo = legacy.showCustomLogoOnLogin ?? authBranding.showLogo;
authBranding.showGreeting = legacy.showCustomGreetingOnLogin ?? authBranding.showGreeting;
}
if (partialAuthBranding.success) Object.assign(authBranding, partialAuthBranding.data);
const result = brandingServerSettingsSchema.safeParse({
...defaultBrandingSettings,
...partialValue.data,
authBranding,
});
if (!result.success) return defaultBrandingSettings;
return result.data;
};
export const defaultServerSettings = {
analytics: {
enableGeneral: true,
@@ -36,6 +145,7 @@ export const defaultServerSettings = {
appearance: {
defaultColorScheme: "auto" as ColorScheme,
},
branding: defaultBrandingSettings,
culture: {
defaultLocale: "en" as SupportedLanguage,
},