+12








fcd7c979df
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com> Co-authored-by: arturict <arturict@users.noreply.github.com> Co-authored-by: Thomas <thomas@ajnart.fr> Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com> Co-authored-by: Danura30082 <danura30082+github@gmail.com> Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com> Co-authored-by: ajnart <thomas@ajnart.fr> Co-authored-by: ajnart <thomas@ajnart.dev> Co-authored-by: chenjun <chenjun@foxmail.com> Co-authored-by: Wikibear <wikibear@users.noreply.github.com> Co-authored-by: Bergasha <bergasha@gmail.com> Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com> Co-authored-by: Lucas Araujo <lucas.lra@gmail.com> Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com> Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social> Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com> Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com> Co-authored-by: thrawnx <tight.bulb9384@fastmail.com> Co-authored-by: Chris <58918656+nordada@users.noreply.github.com> Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com> Co-authored-by: Yanis <naamaneyanis@gmail.com> Co-authored-by: Wikibear <me@sej.de> Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
import type { ColorScheme } from "@homarr/definitions";
|
|
import type { SupportedLanguage } from "@homarr/translation";
|
|
import { z } from "zod/v4";
|
|
|
|
export const defaultServerSettingsKeys = [
|
|
"analytics",
|
|
"crawlingAndIndexing",
|
|
"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),
|
|
customCss: z.string().max(16_384),
|
|
});
|
|
|
|
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",
|
|
customCss: "",
|
|
};
|
|
|
|
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,
|
|
instanceId: null as string | null,
|
|
lastSuccessfulSnapshotAt: null as string | null,
|
|
},
|
|
crawlingAndIndexing: {
|
|
noIndex: true,
|
|
noFollow: true,
|
|
noTranslate: true,
|
|
noSiteLinksSearchBox: false,
|
|
},
|
|
board: {
|
|
homeBoardId: null as string | null,
|
|
mobileHomeBoardId: null as string | null,
|
|
enableStatusByDefault: true,
|
|
forceDisableStatus: false,
|
|
},
|
|
user: {
|
|
enableGravatar: true,
|
|
},
|
|
appearance: {
|
|
defaultColorScheme: "auto" as ColorScheme,
|
|
},
|
|
branding: defaultBrandingSettings,
|
|
culture: {
|
|
defaultLocale: "en" as SupportedLanguage,
|
|
},
|
|
search: {
|
|
defaultSearchEngineId: null as string | null,
|
|
},
|
|
} satisfies ServerSettingsRecord;
|
|
|
|
export type ServerSettings = typeof defaultServerSettings;
|