fix(selfhost): branding image origins and theme colour (#3389)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-11 13:31:05 +02:00
1 parent cca8227ca6
commit edc913096a
27 files changed
+724 -49

No files matched your search

@@ -362,4 +362,17 @@ describe('instance config billing on a hosted instance', () => {
}).execute();
expect(updated.app_public.branding.theme_color).toBe('#123456');
});
it('rejects a theme colour that is not hex', async () => {
const admin = await createAdmin(context.harness);
for (const theme_color of ['rgb(123, 44, 228)', 'rebeccapurple', '#12345']) {
await patchConfig(context.harness, admin, {app_public: {branding: {theme_color}}})
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
.execute();
}
const cleared = await patchConfig(context.harness, admin, {
app_public: {branding: {theme_color: null}},
}).execute();
expect(cleared.app_public.branding.theme_color).toBeNull();
});
});
@@ -219,6 +219,33 @@ describe('InstanceConfigRepository', () => {
expect(config.branding.product_name).toBe('Kept');
});
it('serves the theme colour as six-digit hex and drops a stored value that is not hex', async () => {
setCassandraQueryExecutorForTesting(new InMemoryCassandraQueryExecutor());
const repository = createRepository(new MockKVProvider());
expect((await repository.setAppPublicConfig({branding: {theme_color: '7B2CE4'}})).branding.theme_color).toBe(
'#7b2ce4',
);
expect((await repository.setAppPublicConfig({branding: {theme_color: '#AbC'}})).branding.theme_color).toBe(
'#aabbcc',
);
await repository.setConfig(APP_PUBLIC_CONFIG_KEY, JSON.stringify({branding: {theme_color: 'rgb(123, 44, 228)'}}));
expect((await repository.getAppPublicConfig()).branding.theme_color).toBeNull();
});
it('treats the black theme colour the setup wizard used to store as unset', async () => {
setCassandraQueryExecutorForTesting(new InMemoryCassandraQueryExecutor());
const repository = createRepository(new MockKVProvider());
await repository.setConfig(APP_PUBLIC_CONFIG_KEY, JSON.stringify({branding: {theme_color: '#000000'}}));
expect((await repository.getAppPublicConfig()).branding.theme_color).toBeNull();
expect((await repository.setAppPublicConfig({branding: {theme_color: '#000'}})).branding.theme_color).toBeNull();
expect((await repository.setAppPublicConfig({branding: {theme_color: '#000001'}})).branding.theme_color).toBe(
'#000001',
);
});
it('stores uploaded branding assets as references and resolves them against the current media endpoint', async () => {
setCassandraQueryExecutorForTesting(new CountingInMemoryCassandraQueryExecutor());
const repository = createRepository(new MockKVProvider());
@@ -410,6 +410,14 @@ function normalizeOptionalPublicString(value: string | null | undefined, fallbac
return value === undefined ? fallback : normalizeOptionalString(value);
}
function normalizeThemeColor(value: unknown): string | null {
const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(normalizeOptionalString(value) ?? '');
if (match === null) return null;
const digits = match[1].toLowerCase();
const color = `#${digits.length === 3 ? digits.replace(/./g, '$&$&') : digits}`;
return color === '#000000' ? null : color;
}
function getDefaultPremiumProductName(): string {
return Config.instance.selfHosted ? 'Premium' : 'Plutonium';
}
@@ -423,7 +431,7 @@ function getDefaultAppPublicConfig(): InstanceAppPublicConfig {
logo_url: normalizeOptionalString(Config.instance.branding.logoUrl),
wordmark_url: normalizeOptionalString(Config.instance.branding.wordmarkUrl),
favicon_url: normalizeOptionalString(Config.instance.branding.faviconUrl),
theme_color: normalizeOptionalString(Config.instance.branding.themeColor),
theme_color: normalizeThemeColor(Config.instance.branding.themeColor),
status_page_url: normalizeOptionalString(Config.instance.branding.statusPageUrl),
status_page_incident_history_url: normalizeOptionalString(Config.instance.branding.statusPageIncidentHistoryUrl),
premium_product_name: getDefaultPremiumProductName(),
@@ -740,7 +748,8 @@ function buildAppPublicConfig(config: z.infer<typeof StoredInstanceAppPublicSche
logo_url: normalizeOptionalPublicString(branding.logo_url, defaults.branding.logo_url),
wordmark_url: normalizeOptionalPublicString(branding.wordmark_url, defaults.branding.wordmark_url),
favicon_url: normalizeOptionalPublicString(branding.favicon_url, defaults.branding.favicon_url),
theme_color: normalizeOptionalPublicString(branding.theme_color, defaults.branding.theme_color),
theme_color:
branding.theme_color === undefined ? defaults.branding.theme_color : normalizeThemeColor(branding.theme_color),
status_page_url: normalizeOptionalPublicString(branding.status_page_url, defaults.branding.status_page_url),
status_page_incident_history_url: normalizeOptionalPublicString(
branding.status_page_incident_history_url,