diff --git a/apps/client/src/common/api/project.ts b/apps/client/src/common/api/project.ts index 1fd1cf52f..c31a2f72a 100644 --- a/apps/client/src/common/api/project.ts +++ b/apps/client/src/common/api/project.ts @@ -21,8 +21,9 @@ export async function getProjectData(options?: RequestOptions): Promise> { - return axios.post(projectPath, data); +export async function postProjectData(data: ProjectData): Promise { + const response = await axios.post(projectPath, data); + return response.data; } /** diff --git a/apps/server/src/api-data/project-data/__tests__/projectData.parser.test.ts b/apps/server/src/api-data/project-data/__tests__/projectData.parser.test.ts index 6fe4c1242..d6789a613 100644 --- a/apps/server/src/api-data/project-data/__tests__/projectData.parser.test.ts +++ b/apps/server/src/api-data/project-data/__tests__/projectData.parser.test.ts @@ -7,4 +7,25 @@ describe('parseProjectData()', () => { expect(result).toBeTypeOf('object'); expect(errorEmitter).toHaveBeenCalledOnce(); }); + + it('sanitises malformed custom project data', () => { + const errorEmitter = vi.fn(); + const result = parseProjectData( + { + project: { + title: 'Demo', + description: '', + url: '', + info: '', + logo: null, + // @ts-expect-error -- checking malformed data + custom: '{"networkInterfaces":[{"name":"localhost","address":"127.0.0.1"}]}', + }, + }, + errorEmitter, + ); + + expect(result.custom).toEqual([]); + expect(errorEmitter).toHaveBeenCalledWith('Project custom data is invalid, using defaults'); + }); }); diff --git a/apps/server/src/api-data/project-data/projectData.parser.ts b/apps/server/src/api-data/project-data/projectData.parser.ts index 73ff787cd..9380f767d 100644 --- a/apps/server/src/api-data/project-data/projectData.parser.ts +++ b/apps/server/src/api-data/project-data/projectData.parser.ts @@ -22,6 +22,53 @@ export function parseProjectData(data: Partial, emitError?: Error url: data.project.url ?? defaultProject.url, info: data.project.info ?? defaultProject.info, logo: data.project.logo ?? defaultProject.logo, - custom: data.project.custom ?? defaultProject.custom, + custom: parseCustomProjectData(data.project.custom, defaultProject.custom, emitError), }; } + +function isProjectCustomEntry(entry: unknown): entry is ProjectData['custom'][number] { + if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) { + return false; + } + + const { title, value, url } = entry as Record; + + return typeof title === 'string' && typeof value === 'string' && (url === undefined || typeof url === 'string'); +} + +function parseCustomProjectData( + data: unknown, + defaultCustomData: ProjectData['custom'], + emitError?: ErrorEmitter, +): ProjectData['custom'] { + if (!Array.isArray(data)) { + if (data !== undefined) { + emitError?.('Project custom data is invalid, using defaults'); + } + return defaultCustomData; + } + + const parsed: ProjectData['custom'] = []; + let skippedInvalidEntry = false; + + for (let i = 0; i < data.length; i++) { + const entry = data[i]; + + if (!isProjectCustomEntry(entry)) { + skippedInvalidEntry = true; + continue; + } + + parsed.push({ + title: entry.title, + value: entry.value, + url: entry.url ?? '', + }); + } + + if (skippedInvalidEntry) { + emitError?.('Project custom data contained invalid entries, skipping them'); + } + + return parsed; +}