diff --git a/apps/backend/lambdas/projects/handler.ts b/apps/backend/lambdas/projects/handler.ts index f82ef6ae..32da8e11 100644 --- a/apps/backend/lambdas/projects/handler.ts +++ b/apps/backend/lambdas/projects/handler.ts @@ -208,12 +208,26 @@ export const handler = async (event: any): Promise => { if (rawPath.startsWith('/') && rawPath.split('/').length === 2 && method === 'PUT') { const id = rawPath.split('/')[1]; if (!id) return json(400, { message: 'id is required' }); - const body = event.body ? JSON.parse(event.body) as Record : {}; + let body: Record; + try { + body = event.body ? JSON.parse(event.body) as Record : {}; + } catch (e) { + return json(400, { message: 'Invalid JSON in request body' }); + } + + const result = ProjectValidationUtils.buildUpdateValues(body); + if (!result.isValid) return json(400, { message: result.error }); + const updateValues = result.values!; + + if (Object.keys(updateValues).length === 0) { + return json(400, { message: 'No valid fields provided' }); + } + const updatedProject = await db .updateTable("branch.projects") - .set(body) + .set(updateValues) .where("project_id", "=", Number(id)) - .returning(["project_id", "name", "description", "total_budget"]) // control returned fields + .returning(["project_id", "name", "description", "total_budget"]) .executeTakeFirst(); if (!updatedProject) return json(404, { message: `Project not found for id: ${id}` }); return json(200, updatedProject); diff --git a/apps/backend/lambdas/projects/test/crud.test.ts b/apps/backend/lambdas/projects/test/crud.test.ts index 961869ec..478140bb 100644 --- a/apps/backend/lambdas/projects/test/crud.test.ts +++ b/apps/backend/lambdas/projects/test/crud.test.ts @@ -97,3 +97,29 @@ test("project get 400 test 🌞", async () => { let body = await res.json(); expect(body.message).toBe("Project not found for id: 1000"); }); + +test("update project ignores protected fields 🌞", async () => { + const beforeRes = await fetch("http://localhost:3000/projects/1"); + expect(beforeRes.status).toBe(200); + const before = await beforeRes.json(); + + const res = await fetch("http://localhost:3000/projects/1", { + method: "PUT", + body: JSON.stringify({ + name: "Project 1 Sanitized", + project_id: 9999, + created_at: "2000-01-01T00:00:00.000Z", + }), + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.project_id).toBe(1); + expect(body.name).toBe("Project 1 Sanitized"); + + const afterRes = await fetch("http://localhost:3000/projects/1"); + expect(afterRes.status).toBe(200); + const after = await afterRes.json(); + expect(after.project_id).toBe(1); + expect(after.created_at).toBe(before.created_at); +}); \ No newline at end of file diff --git a/apps/backend/lambdas/projects/validation-utils.ts b/apps/backend/lambdas/projects/validation-utils.ts index 16cb9b87..f9f3fc44 100644 --- a/apps/backend/lambdas/projects/validation-utils.ts +++ b/apps/backend/lambdas/projects/validation-utils.ts @@ -87,4 +87,67 @@ export class ProjectValidationUtils { } return { isValid: true, value: d.length === 0 ? '' : d }; } + static buildUpdateValues(body: Record): { isValid: boolean; error?: string; values?: Record } { + const updateValues: Record = {}; + + if ('name' in body) { + const nameResult = ProjectValidationUtils.validateName(body.name); + if (!nameResult.isValid) return { isValid: false, error: nameResult.error }; + updateValues.name = nameResult.value; + } + + if ('description' in body) { + if (body.description === undefined || body.description === null) { + updateValues.description = ''; + } else if (typeof body.description !== 'string') { + return { isValid: false, error: "'description' must be a string" }; + } else { + const description = body.description.trim(); + if (description.length > 1000) { + return { isValid: false, error: "'description' must be <= 1000 chars" }; + } + updateValues.description = description; + } + } + + if ('total_budget' in body) { + const parsedBudget = ProjectValidationUtils.parseNumericToFixed(body.total_budget); + if (parsedBudget === 'INVALID') return { isValid: false, error: "'total_budget' must be a number" }; + updateValues.total_budget = parsedBudget; + } + + if ('currency' in body) { + if (body.currency === undefined || body.currency === null) { + updateValues.currency = null; + } else if (typeof body.currency !== 'string') { + return { isValid: false, error: "'currency' must be 1-10 chars" }; + } else { + const currencyResult = ProjectValidationUtils.validateCurrency(body.currency); + if (!currencyResult.isValid) return { isValid: false, error: currencyResult.error }; + updateValues.currency = currencyResult.value; + } + } + + if ('start_date' in body) { + if (body.start_date === undefined || body.start_date === null || body.start_date === '') { + updateValues.start_date = null; + } else if (typeof body.start_date !== 'string' || !ProjectValidationUtils.isValidDate(body.start_date)) { + return { isValid: false, error: "'start_date' must be YYYY-MM-DD" }; + } else { + updateValues.start_date = body.start_date; + } + } + + if ('end_date' in body) { + if (body.end_date === undefined || body.end_date === null || body.end_date === '') { + updateValues.end_date = null; + } else if (typeof body.end_date !== 'string' || !ProjectValidationUtils.isValidDate(body.end_date)) { + return { isValid: false, error: "'end_date' must be YYYY-MM-DD" }; + } else { + updateValues.end_date = body.end_date; + } + } + + return { isValid: true, values: updateValues }; +} }