@actuate-media/cms-core 0.35.1 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/api/appearance-routes.test.d.ts +2 -0
- package/dist/__tests__/api/appearance-routes.test.d.ts.map +1 -0
- package/dist/__tests__/api/appearance-routes.test.js +134 -0
- package/dist/__tests__/api/appearance-routes.test.js.map +1 -0
- package/dist/__tests__/appearance/appearance.test.d.ts +2 -0
- package/dist/__tests__/appearance/appearance.test.d.ts.map +1 -0
- package/dist/__tests__/appearance/appearance.test.js +273 -0
- package/dist/__tests__/appearance/appearance.test.js.map +1 -0
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +61 -0
- package/dist/api/handlers.js.map +1 -1
- package/dist/appearance/color.d.ts +24 -0
- package/dist/appearance/color.d.ts.map +1 -0
- package/dist/appearance/color.js +90 -0
- package/dist/appearance/color.js.map +1 -0
- package/dist/appearance/index.d.ts +13 -0
- package/dist/appearance/index.d.ts.map +1 -0
- package/dist/appearance/index.js +5 -0
- package/dist/appearance/index.js.map +1 -0
- package/dist/appearance/logo-shape.d.ts +26 -0
- package/dist/appearance/logo-shape.d.ts.map +1 -0
- package/dist/appearance/logo-shape.js +62 -0
- package/dist/appearance/logo-shape.js.map +1 -0
- package/dist/appearance/resolve.d.ts +46 -0
- package/dist/appearance/resolve.d.ts.map +1 -0
- package/dist/appearance/resolve.js +208 -0
- package/dist/appearance/resolve.js.map +1 -0
- package/dist/appearance/theme-tokens.d.ts +50 -0
- package/dist/appearance/theme-tokens.d.ts.map +1 -0
- package/dist/appearance/theme-tokens.js +198 -0
- package/dist/appearance/theme-tokens.js.map +1 -0
- package/dist/appearance/types.d.ts +110 -0
- package/dist/appearance/types.d.ts.map +1 -0
- package/dist/appearance/types.js +16 -0
- package/dist/appearance/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appearance-routes.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/appearance-routes.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { handleActuateAPI } from '../../api/index.js';
|
|
3
|
+
import { initDB } from '../../db.js';
|
|
4
|
+
import { generateApiKey } from '../../security/api-key-enhanced.js';
|
|
5
|
+
/**
|
|
6
|
+
* Contract tests for the appearance/brand endpoints:
|
|
7
|
+
* - GET /public/brand → public, exposes ONLY the public brand + favicon.
|
|
8
|
+
* - GET /appearance → authed, exposes stored settings + resolved views.
|
|
9
|
+
*
|
|
10
|
+
* Pins the response shapes the public frontend and admin Appearance tab depend
|
|
11
|
+
* on, and proves the public route never leaks the admin-only brand.
|
|
12
|
+
*/
|
|
13
|
+
const VALID_SECRET = 'a'.repeat(48);
|
|
14
|
+
const SETTINGS_BLOB = {
|
|
15
|
+
siteTitle: 'Actuate Media',
|
|
16
|
+
brand: {
|
|
17
|
+
mode: 'customAdminBrand',
|
|
18
|
+
publicPrimaryLogoAssetId: 'pub-logo',
|
|
19
|
+
adminPrimaryLogoAssetId: 'admin-logo',
|
|
20
|
+
faviconSourceAssetId: 'fav',
|
|
21
|
+
updatedAt: '2026-02-03T00:00:00.000Z',
|
|
22
|
+
},
|
|
23
|
+
appearance: { adminAccentColor: 'teal', sidebarDensity: 'compact', reduceMotion: true },
|
|
24
|
+
};
|
|
25
|
+
const MEDIA_ROWS = [
|
|
26
|
+
{
|
|
27
|
+
id: 'pub-logo',
|
|
28
|
+
publicUrl: 'https://cdn.example.com/pub.svg',
|
|
29
|
+
mimeType: 'image/svg+xml',
|
|
30
|
+
width: 400,
|
|
31
|
+
height: 100,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'admin-logo',
|
|
35
|
+
publicUrl: 'https://cdn.example.com/admin.svg',
|
|
36
|
+
mimeType: 'image/svg+xml',
|
|
37
|
+
width: 380,
|
|
38
|
+
height: 90,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'fav',
|
|
42
|
+
publicUrl: 'https://cdn.example.com/fav.png',
|
|
43
|
+
mimeType: 'image/png',
|
|
44
|
+
width: 32,
|
|
45
|
+
height: 32,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
function createMockDB(opts = {}) {
|
|
49
|
+
const apiKey = opts.apiKey;
|
|
50
|
+
return {
|
|
51
|
+
apiKey: {
|
|
52
|
+
findMany: async () => (apiKey ? [apiKey] : []),
|
|
53
|
+
findUnique: async ({ where }) => apiKey && apiKey.keyHash === where.keyHash ? apiKey : null,
|
|
54
|
+
update: async () => apiKey,
|
|
55
|
+
},
|
|
56
|
+
document: {
|
|
57
|
+
findFirst: async () => ({ data: SETTINGS_BLOB }),
|
|
58
|
+
findMany: async () => [],
|
|
59
|
+
count: async () => 0,
|
|
60
|
+
},
|
|
61
|
+
media: {
|
|
62
|
+
findMany: async () => MEDIA_ROWS,
|
|
63
|
+
},
|
|
64
|
+
user: {},
|
|
65
|
+
session: {},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function makeKey(scopes) {
|
|
69
|
+
const { key, keyHash, keyPrefix } = await generateApiKey({ prefix: 'act_sk', scopes });
|
|
70
|
+
return {
|
|
71
|
+
key,
|
|
72
|
+
record: {
|
|
73
|
+
id: 'apikey-1',
|
|
74
|
+
keyHash,
|
|
75
|
+
keyPrefix,
|
|
76
|
+
userId: 'user-1',
|
|
77
|
+
scopes,
|
|
78
|
+
ipRestrictions: null,
|
|
79
|
+
expiresAt: null,
|
|
80
|
+
lastUsedAt: null,
|
|
81
|
+
revokedAt: null,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
describe('GET /public/brand', () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
delete globalThis.__actuateConfig;
|
|
88
|
+
process.env.CMS_SECRET = VALID_SECRET;
|
|
89
|
+
});
|
|
90
|
+
it('returns the public brand + favicon without auth and hides the admin brand', async () => {
|
|
91
|
+
const db = createMockDB();
|
|
92
|
+
initDB(db);
|
|
93
|
+
const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
|
|
94
|
+
const res = await handler(new Request('https://example.com/api/cms/public/brand'));
|
|
95
|
+
expect(res.status).toBe(200);
|
|
96
|
+
const body = (await res.json());
|
|
97
|
+
expect(body.data.public.primary.url).toBe('https://cdn.example.com/pub.svg');
|
|
98
|
+
expect(body.data.favicon.icon).toContain('https://cdn.example.com/fav.png');
|
|
99
|
+
// The admin-only brand must NOT be exposed publicly.
|
|
100
|
+
expect(body.data.admin).toBeUndefined();
|
|
101
|
+
expect(JSON.stringify(body)).not.toContain('admin.svg');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('GET /appearance', () => {
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
delete globalThis.__actuateConfig;
|
|
107
|
+
process.env.CMS_SECRET = VALID_SECRET;
|
|
108
|
+
});
|
|
109
|
+
it('requires authentication', async () => {
|
|
110
|
+
const db = createMockDB();
|
|
111
|
+
initDB(db);
|
|
112
|
+
const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
|
|
113
|
+
const res = await handler(new Request('https://example.com/api/cms/appearance'));
|
|
114
|
+
expect(res.status).toBe(401);
|
|
115
|
+
});
|
|
116
|
+
it('returns stored appearance + brand + resolved admin/public views when authed', async () => {
|
|
117
|
+
const { key, record } = await makeKey({ admin: true });
|
|
118
|
+
const db = createMockDB({ apiKey: record });
|
|
119
|
+
initDB(db);
|
|
120
|
+
const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
|
|
121
|
+
const res = await handler(new Request('https://example.com/api/cms/appearance', {
|
|
122
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
123
|
+
}));
|
|
124
|
+
expect(res.status).toBe(200);
|
|
125
|
+
const body = (await res.json());
|
|
126
|
+
expect(body.data.appearance.adminAccentColor).toBe('teal');
|
|
127
|
+
expect(body.data.appearance.sidebarDensity).toBe('compact');
|
|
128
|
+
expect(body.data.brand.mode).toBe('customAdminBrand');
|
|
129
|
+
// customAdminBrand → admin primary resolves to the admin asset.
|
|
130
|
+
expect(body.data.resolved.admin.primary.url).toBe('https://cdn.example.com/admin.svg');
|
|
131
|
+
expect(body.data.resolved.public.primary.url).toBe('https://cdn.example.com/pub.svg');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
//# sourceMappingURL=appearance-routes.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appearance-routes.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/appearance-routes.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appearance.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/appearance/appearance.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { classifyLogoShape, logoShapeWarning, contrastRatio, isParseableColor, validateAccentColor, generateAdminThemeTokens, accentTokensToCssVars, ACCENT_PRESETS, siteInitials, resolveAdminBrand, resolvePublicBrand, getFaviconMetadata, parseAppearance, parseBrand, brandAssetIds, resolveBrand, DEFAULT_APPEARANCE, } from '../../appearance/index.js';
|
|
3
|
+
describe('classifyLogoShape', () => {
|
|
4
|
+
it('classifies by aspect ratio', () => {
|
|
5
|
+
expect(classifyLogoShape(100, 100)).toBe('square');
|
|
6
|
+
expect(classifyLogoShape(120, 100)).toBe('square'); // ratio 1.2
|
|
7
|
+
expect(classifyLogoShape(400, 100)).toBe('horizontal'); // ratio 4
|
|
8
|
+
expect(classifyLogoShape(100, 400)).toBe('vertical'); // ratio 0.25
|
|
9
|
+
expect(classifyLogoShape(160, 100)).toBe('neutral'); // ratio 1.6
|
|
10
|
+
});
|
|
11
|
+
it('returns unknown for missing/invalid dimensions', () => {
|
|
12
|
+
expect(classifyLogoShape(undefined, undefined)).toBe('unknown');
|
|
13
|
+
expect(classifyLogoShape(0, 100)).toBe('unknown');
|
|
14
|
+
expect(classifyLogoShape(100, null)).toBe('unknown');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('logoShapeWarning', () => {
|
|
18
|
+
it('warns when a square is used as a primary logo', () => {
|
|
19
|
+
expect(logoShapeWarning('publicPrimaryLogo', 'square')).toMatch(/square mark/i);
|
|
20
|
+
});
|
|
21
|
+
it('warns when a horizontal is used as a logo mark', () => {
|
|
22
|
+
expect(logoShapeWarning('adminLogoMark', 'horizontal')).toMatch(/square mark works better/i);
|
|
23
|
+
});
|
|
24
|
+
it('warns when a non-square favicon source is uploaded', () => {
|
|
25
|
+
expect(logoShapeWarning('faviconSource', 'horizontal')).toMatch(/not square/i);
|
|
26
|
+
});
|
|
27
|
+
it('warns about vertical logos in wide slots', () => {
|
|
28
|
+
expect(logoShapeWarning('publicPrimaryLogo', 'vertical')).toMatch(/vertical/i);
|
|
29
|
+
});
|
|
30
|
+
it('is silent for good fits and unknown shapes', () => {
|
|
31
|
+
expect(logoShapeWarning('publicPrimaryLogo', 'horizontal')).toBeNull();
|
|
32
|
+
expect(logoShapeWarning('publicLogoMark', 'square')).toBeNull();
|
|
33
|
+
expect(logoShapeWarning('publicPrimaryLogo', 'unknown')).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('color + contrast', () => {
|
|
37
|
+
it('computes the canonical black/white contrast as 21:1', () => {
|
|
38
|
+
expect(contrastRatio('#000000', '#ffffff')).toBeCloseTo(21, 0);
|
|
39
|
+
});
|
|
40
|
+
it('returns 1 for identical colors', () => {
|
|
41
|
+
expect(contrastRatio('#ffffff', '#ffffff')).toBeCloseTo(1, 5);
|
|
42
|
+
});
|
|
43
|
+
it('parses oklch and hex', () => {
|
|
44
|
+
expect(isParseableColor('oklch(0.54 0.23 293)')).toBe(true);
|
|
45
|
+
expect(isParseableColor('#abc')).toBe(true);
|
|
46
|
+
expect(isParseableColor('rebeccapurple')).toBe(false);
|
|
47
|
+
expect(isParseableColor('')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it('ranks a dark oklch accent as high contrast against white', () => {
|
|
50
|
+
// The purple preset on white should clear the 3:1 UI bar comfortably.
|
|
51
|
+
expect(contrastRatio('oklch(0.54 0.23 293)', '#ffffff')).toBeGreaterThan(3);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('validateAccentColor', () => {
|
|
55
|
+
it('accepts every preset', () => {
|
|
56
|
+
for (const name of Object.keys(ACCENT_PRESETS)) {
|
|
57
|
+
expect(validateAccentColor(name).valid).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
it('rejects an unparseable color', () => {
|
|
61
|
+
const r = validateAccentColor('not-a-color');
|
|
62
|
+
expect(r.valid).toBe(false);
|
|
63
|
+
expect(r.reason).toMatch(/valid hex or oklch/i);
|
|
64
|
+
});
|
|
65
|
+
it('rejects a custom color that fails contrast against white', () => {
|
|
66
|
+
const r = validateAccentColor('#fff8dc'); // near-white, low contrast
|
|
67
|
+
expect(r.valid).toBe(false);
|
|
68
|
+
expect(r.reason).toMatch(/below the 3:1/i);
|
|
69
|
+
});
|
|
70
|
+
it('accepts a sufficiently dark custom color', () => {
|
|
71
|
+
expect(validateAccentColor('#0a3d62').valid).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('generateAdminThemeTokens', () => {
|
|
75
|
+
it('returns preset tokens for light and dark', () => {
|
|
76
|
+
expect(generateAdminThemeTokens({ adminAccentColor: 'blue' }, 'light')).toEqual(ACCENT_PRESETS.blue.light);
|
|
77
|
+
expect(generateAdminThemeTokens({ adminAccentColor: 'blue' }, 'dark')).toEqual(ACCENT_PRESETS.blue.dark);
|
|
78
|
+
});
|
|
79
|
+
it('drives tokens from a custom color', () => {
|
|
80
|
+
const tokens = generateAdminThemeTokens({ adminAccentColor: '#0a3d62' }, 'light');
|
|
81
|
+
expect(tokens.brand).toBe('#0a3d62');
|
|
82
|
+
expect(tokens.primary).toBe('#0a3d62');
|
|
83
|
+
});
|
|
84
|
+
it('falls back to purple for an unknown name', () => {
|
|
85
|
+
expect(generateAdminThemeTokens({ adminAccentColor: 'chartreuse' }, 'light')).toEqual(ACCENT_PRESETS.purple.light);
|
|
86
|
+
});
|
|
87
|
+
it('maps tokens to the expected CSS variables', () => {
|
|
88
|
+
const vars = accentTokensToCssVars(ACCENT_PRESETS.purple.light);
|
|
89
|
+
expect(Object.keys(vars).sort()).toEqual([
|
|
90
|
+
'--brand',
|
|
91
|
+
'--brand-foreground',
|
|
92
|
+
'--primary',
|
|
93
|
+
'--primary-foreground',
|
|
94
|
+
'--ring',
|
|
95
|
+
'--sidebar-primary',
|
|
96
|
+
'--sidebar-primary-foreground',
|
|
97
|
+
].sort());
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('siteInitials', () => {
|
|
101
|
+
it('derives up to two initials', () => {
|
|
102
|
+
expect(siteInitials('Actuate Media')).toBe('AM');
|
|
103
|
+
expect(siteInitials('Actuate')).toBe('AC');
|
|
104
|
+
expect(siteInitials('')).toBe('A');
|
|
105
|
+
expect(siteInitials(undefined)).toBe('A');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
function assetMap(...assets) {
|
|
109
|
+
return new Map(assets.map((a) => [a.id, a]));
|
|
110
|
+
}
|
|
111
|
+
const HORIZONTAL = {
|
|
112
|
+
id: 'primary-1',
|
|
113
|
+
publicUrl: 'https://cdn.example.com/logo.svg',
|
|
114
|
+
mimeType: 'image/svg+xml',
|
|
115
|
+
width: 400,
|
|
116
|
+
height: 100,
|
|
117
|
+
};
|
|
118
|
+
const MARK = {
|
|
119
|
+
id: 'mark-1',
|
|
120
|
+
publicUrl: 'https://cdn.example.com/mark.svg',
|
|
121
|
+
mimeType: 'image/svg+xml',
|
|
122
|
+
width: 64,
|
|
123
|
+
height: 64,
|
|
124
|
+
};
|
|
125
|
+
const FAVICON = {
|
|
126
|
+
id: 'fav-1',
|
|
127
|
+
publicUrl: 'https://cdn.example.com/favicon.png',
|
|
128
|
+
mimeType: 'image/png',
|
|
129
|
+
width: 32,
|
|
130
|
+
height: 32,
|
|
131
|
+
};
|
|
132
|
+
describe('resolveAdminBrand', () => {
|
|
133
|
+
it('inherits the public brand by default', () => {
|
|
134
|
+
const brand = {
|
|
135
|
+
mode: 'inheritPublicBrand',
|
|
136
|
+
publicPrimaryLogoAssetId: 'primary-1',
|
|
137
|
+
publicLogoMarkAssetId: 'mark-1',
|
|
138
|
+
};
|
|
139
|
+
const r = resolveAdminBrand(brand, assetMap(HORIZONTAL, MARK), 'Actuate Media');
|
|
140
|
+
expect(r.inherited).toBe(true);
|
|
141
|
+
expect(r.primary?.url).toBe(HORIZONTAL.publicUrl);
|
|
142
|
+
expect(r.mark?.url).toBe(MARK.publicUrl);
|
|
143
|
+
expect(r.primary?.shape).toBe('horizontal');
|
|
144
|
+
expect(r.initials).toBe('AM');
|
|
145
|
+
});
|
|
146
|
+
it('prefers admin assets in customAdminBrand mode, falling back to public', () => {
|
|
147
|
+
const adminPrimary = { ...HORIZONTAL, id: 'admin-primary' };
|
|
148
|
+
const brand = {
|
|
149
|
+
mode: 'customAdminBrand',
|
|
150
|
+
adminPrimaryLogoAssetId: 'admin-primary',
|
|
151
|
+
publicLogoMarkAssetId: 'mark-1',
|
|
152
|
+
};
|
|
153
|
+
const r = resolveAdminBrand(brand, assetMap(adminPrimary, MARK), 'Actuate Media');
|
|
154
|
+
expect(r.inherited).toBe(false);
|
|
155
|
+
expect(r.primary?.url).toBe(adminPrimary.publicUrl);
|
|
156
|
+
// No admin mark → falls back to the public mark.
|
|
157
|
+
expect(r.mark?.url).toBe(MARK.publicUrl);
|
|
158
|
+
});
|
|
159
|
+
it('falls back the collapsed mark to the favicon source', () => {
|
|
160
|
+
const brand = {
|
|
161
|
+
mode: 'inheritPublicBrand',
|
|
162
|
+
publicPrimaryLogoAssetId: 'primary-1',
|
|
163
|
+
faviconSourceAssetId: 'fav-1',
|
|
164
|
+
};
|
|
165
|
+
const r = resolveAdminBrand(brand, assetMap(HORIZONTAL, FAVICON), 'Actuate Media');
|
|
166
|
+
expect(r.mark?.url).toBe(FAVICON.publicUrl);
|
|
167
|
+
});
|
|
168
|
+
it('returns null logos and initials when nothing is configured', () => {
|
|
169
|
+
const r = resolveAdminBrand({ mode: 'inheritPublicBrand' }, assetMap(), 'Globex Corp');
|
|
170
|
+
expect(r.primary).toBeNull();
|
|
171
|
+
expect(r.mark).toBeNull();
|
|
172
|
+
expect(r.initials).toBe('GC');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('resolvePublicBrand', () => {
|
|
176
|
+
it('resolves public assets and mark fallback', () => {
|
|
177
|
+
const brand = {
|
|
178
|
+
mode: 'customAdminBrand', // irrelevant to public resolution
|
|
179
|
+
publicPrimaryLogoAssetId: 'primary-1',
|
|
180
|
+
faviconSourceAssetId: 'fav-1',
|
|
181
|
+
};
|
|
182
|
+
const r = resolvePublicBrand(brand, assetMap(HORIZONTAL, FAVICON), 'Actuate Media');
|
|
183
|
+
expect(r.primary?.url).toBe(HORIZONTAL.publicUrl);
|
|
184
|
+
expect(r.mark?.url).toBe(FAVICON.publicUrl);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe('getFaviconMetadata', () => {
|
|
188
|
+
it('returns null icons when no favicon source is set', () => {
|
|
189
|
+
const r = getFaviconMetadata({ mode: 'inheritPublicBrand' }, assetMap());
|
|
190
|
+
expect(r.icon).toBeNull();
|
|
191
|
+
expect(r.apple).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
it('cache-busts the icon URL from updatedAt', () => {
|
|
194
|
+
const brand = {
|
|
195
|
+
mode: 'inheritPublicBrand',
|
|
196
|
+
faviconSourceAssetId: 'fav-1',
|
|
197
|
+
updatedAt: '2026-02-03T00:00:00.000Z',
|
|
198
|
+
};
|
|
199
|
+
const r = getFaviconMetadata(brand, assetMap(FAVICON));
|
|
200
|
+
expect(r.icon).toBe(`${FAVICON.publicUrl}?v=${Date.parse(brand.updatedAt)}`);
|
|
201
|
+
expect(r.apple).toBe(r.icon);
|
|
202
|
+
expect(r.type).toBe('image/png');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe('parse + ids', () => {
|
|
206
|
+
it('parses appearance with defaults', () => {
|
|
207
|
+
expect(parseAppearance({})).toEqual(DEFAULT_APPEARANCE);
|
|
208
|
+
expect(parseAppearance({ appearance: { adminAccentColor: 'teal' } }).adminAccentColor).toBe('teal');
|
|
209
|
+
});
|
|
210
|
+
it('parses brand with defaults', () => {
|
|
211
|
+
expect(parseBrand({}).mode).toBe('inheritPublicBrand');
|
|
212
|
+
});
|
|
213
|
+
it('collects unique asset ids', () => {
|
|
214
|
+
const brand = {
|
|
215
|
+
mode: 'customAdminBrand',
|
|
216
|
+
publicPrimaryLogoAssetId: 'a',
|
|
217
|
+
adminPrimaryLogoAssetId: 'a', // duplicate
|
|
218
|
+
faviconSourceAssetId: 'b',
|
|
219
|
+
generatedFaviconAssetIds: ['c', 'b'],
|
|
220
|
+
};
|
|
221
|
+
expect(brandAssetIds(brand).sort()).toEqual(['a', 'b', 'c']);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('resolveBrand (async)', () => {
|
|
225
|
+
it('reads the settings global and resolves every view', async () => {
|
|
226
|
+
const db = {
|
|
227
|
+
document: {
|
|
228
|
+
findFirst: async () => ({
|
|
229
|
+
data: {
|
|
230
|
+
siteTitle: 'Actuate Media',
|
|
231
|
+
brand: {
|
|
232
|
+
mode: 'inheritPublicBrand',
|
|
233
|
+
publicPrimaryLogoAssetId: 'primary-1',
|
|
234
|
+
faviconSourceAssetId: 'fav-1',
|
|
235
|
+
updatedAt: '2026-02-03T00:00:00.000Z',
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
},
|
|
240
|
+
media: {
|
|
241
|
+
findMany: async () => [
|
|
242
|
+
{
|
|
243
|
+
id: 'primary-1',
|
|
244
|
+
publicUrl: HORIZONTAL.publicUrl,
|
|
245
|
+
mimeType: 'image/svg+xml',
|
|
246
|
+
width: 400,
|
|
247
|
+
height: 100,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: 'fav-1',
|
|
251
|
+
publicUrl: FAVICON.publicUrl,
|
|
252
|
+
mimeType: 'image/png',
|
|
253
|
+
width: 32,
|
|
254
|
+
height: 32,
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
const r = await resolveBrand(db, null);
|
|
260
|
+
expect(r.admin.primary?.url).toBe(HORIZONTAL.publicUrl);
|
|
261
|
+
expect(r.public.primary?.url).toBe(HORIZONTAL.publicUrl);
|
|
262
|
+
expect(r.favicon.icon).toContain(FAVICON.publicUrl);
|
|
263
|
+
expect(r.admin.initials).toBe('AM');
|
|
264
|
+
});
|
|
265
|
+
it('degrades gracefully with no media table', async () => {
|
|
266
|
+
const db = { document: { findFirst: async () => ({ data: {} }) } };
|
|
267
|
+
const r = await resolveBrand(db, 'Globex');
|
|
268
|
+
expect(r.admin.primary).toBeNull();
|
|
269
|
+
expect(r.favicon.icon).toBeNull();
|
|
270
|
+
expect(r.public.initials).toBe('GL');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
//# sourceMappingURL=appearance.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appearance.test.js","sourceRoot":"","sources":["../../../src/__tests__/appearance/appearance.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AA+/B5C,iFAAiF;AACjF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AA8JD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AAyRD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AA+/B5C,iFAAiF;AACjF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AA8JD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AAyRD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAwxUzD"}
|
package/dist/api/handlers.js
CHANGED
|
@@ -8897,6 +8897,58 @@ export function registerCMSRoutes(router) {
|
|
|
8897
8897
|
return internalError(err);
|
|
8898
8898
|
}
|
|
8899
8899
|
});
|
|
8900
|
+
// Public brand: resolved logos + favicon for the public frontend. No auth —
|
|
8901
|
+
// brand assets are public site data. Only the PUBLIC brand view + favicon are
|
|
8902
|
+
// exposed here (never the admin-only brand) so a white-labeled admin doesn't
|
|
8903
|
+
// leak through the public API.
|
|
8904
|
+
router.get('/public/brand', async (request) => {
|
|
8905
|
+
try {
|
|
8906
|
+
const { resolveBrand } = await import('../appearance/index.js');
|
|
8907
|
+
const resolved = await resolveBrand(db(), null);
|
|
8908
|
+
return cachedRead(request, {
|
|
8909
|
+
cache: getActuateConfig()?.cache,
|
|
8910
|
+
tags: [ACTUATE_TAG_ROOT, actuateTag('global', 'settings')],
|
|
8911
|
+
wrap: jsonWithCache,
|
|
8912
|
+
build: async () => ({
|
|
8913
|
+
data: { public: resolved.public, favicon: resolved.favicon },
|
|
8914
|
+
}),
|
|
8915
|
+
});
|
|
8916
|
+
}
|
|
8917
|
+
catch (err) {
|
|
8918
|
+
return internalError(err, 'public/brand GET');
|
|
8919
|
+
}
|
|
8920
|
+
});
|
|
8921
|
+
// Authenticated appearance read: stored appearance + brand settings plus the
|
|
8922
|
+
// fully resolved admin/public/favicon views (with asset URLs) so the admin
|
|
8923
|
+
// Appearance tab can render live previews. View-only auth — writes go through
|
|
8924
|
+
// PUT /globals/settings (ADMIN_ROLES), so editors can view but not save.
|
|
8925
|
+
router.get('/appearance', async (request) => {
|
|
8926
|
+
try {
|
|
8927
|
+
const auth = await requireAuth(request);
|
|
8928
|
+
if (auth.error)
|
|
8929
|
+
return auth.error;
|
|
8930
|
+
const { getAppearanceSettings, getBrandSettings, resolveBrand } = await import('../appearance/index.js');
|
|
8931
|
+
const [appearance, brand, resolved] = await Promise.all([
|
|
8932
|
+
getAppearanceSettings(db()),
|
|
8933
|
+
getBrandSettings(db()),
|
|
8934
|
+
resolveBrand(db(), null),
|
|
8935
|
+
]);
|
|
8936
|
+
return json({
|
|
8937
|
+
data: {
|
|
8938
|
+
appearance,
|
|
8939
|
+
brand,
|
|
8940
|
+
resolved: { admin: resolved.admin, public: resolved.public, favicon: resolved.favicon },
|
|
8941
|
+
// Direct per-asset URLs so the editor shows each field's actual
|
|
8942
|
+
// asset (not the resolved fallback chain).
|
|
8943
|
+
assets: resolved.assets,
|
|
8944
|
+
siteTitle: resolved.siteTitle,
|
|
8945
|
+
},
|
|
8946
|
+
});
|
|
8947
|
+
}
|
|
8948
|
+
catch (err) {
|
|
8949
|
+
return internalError(err, 'appearance GET');
|
|
8950
|
+
}
|
|
8951
|
+
});
|
|
8900
8952
|
router.get('/globals/:slug', async (request, params) => {
|
|
8901
8953
|
try {
|
|
8902
8954
|
const auth = await requireAuth(request);
|
|
@@ -8946,8 +8998,17 @@ export function registerCMSRoutes(router) {
|
|
|
8946
8998
|
// Audit settings changes (title/tagline/URL/locale/timezone/etc.) with a
|
|
8947
8999
|
// per-key before/after diff so the audit trail records what changed and
|
|
8948
9000
|
// who changed it. Scalar values only — we never log nested secret blobs.
|
|
9001
|
+
// Nested config blobs (`appearance`, `brand`) are logged as a changed
|
|
9002
|
+
// key (not their contents) so appearance/branding edits are auditable
|
|
9003
|
+
// without dumping asset IDs or theme objects into the log.
|
|
8949
9004
|
try {
|
|
8950
9005
|
const changes = diffScalarKeys(previous ?? {}, body);
|
|
9006
|
+
const prev = (previous ?? {});
|
|
9007
|
+
for (const key of ['appearance', 'brand']) {
|
|
9008
|
+
if (key in body && JSON.stringify(prev[key]) !== JSON.stringify(body[key])) {
|
|
9009
|
+
changes.push({ key, from: '[changed]', to: '[changed]' });
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
8951
9012
|
if (changes.length > 0) {
|
|
8952
9013
|
void logEvent({
|
|
8953
9014
|
event: 'settings_changed',
|