Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions apps/backend/lambdas/projects/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,26 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {
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<string, { name: string, total_budget: number }> : {};
let body: Record<string, unknown>;
try {
body = event.body ? JSON.parse(event.body) as Record<string, unknown> : {};
} 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);
Expand Down
26 changes: 26 additions & 0 deletions apps/backend/lambdas/projects/test/crud.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
63 changes: 63 additions & 0 deletions apps/backend/lambdas/projects/validation-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,67 @@ export class ProjectValidationUtils {
}
return { isValid: true, value: d.length === 0 ? '' : d };
}
static buildUpdateValues(body: Record<string, unknown>): { isValid: boolean; error?: string; values?: Record<string, unknown> } {
const updateValues: Record<string, unknown> = {};

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 };
}
}
Loading