diff --git a/backend/src/services/customerHoursService.js b/backend/src/services/customerHoursService.js index ab24efd6..cbcd792a 100644 --- a/backend/src/services/customerHoursService.js +++ b/backend/src/services/customerHoursService.js @@ -233,9 +233,20 @@ async function createEntry(customerId, payload, adminId) { created_at: new Date(), updated_at: new Date(), }; - // Migration 118 — optional "book to project" link. + // Migration 118 — optional "book to project" link. A project belongs to + // at most one customer, so reject booking hours onto a project owned by a + // DIFFERENT customer (defence-in-depth behind the customer-scoped picker). + // Unassigned projects (customer_account_id null) are allowed for anyone. if (payload.projectId !== undefined && await hasColumnCached('customer_hour_entries', 'project_id')) { - row.project_id = payload.projectId || null; + const projectId = payload.projectId || null; + if (projectId && await trx.schema.hasTable('projects')) { + const project = await trx('projects').where({ id: projectId }).select('customer_account_id').first(); + if (!project) throw new AppError('Project not found', 404, 'PROJECT_NOT_FOUND'); + if (project.customer_account_id != null && project.customer_account_id !== customer.id) { + throw new AppError('That project belongs to a different customer', 422, 'PROJECT_CUSTOMER_MISMATCH'); + } + } + row.project_id = projectId; } const inserted = await trx('customer_hour_entries').insert(row).returning('id'); const entryId = typeof inserted[0] === 'object' ? inserted[0].id : inserted[0]; diff --git a/frontend/src/components/admin/HoursSection.tsx b/frontend/src/components/admin/HoursSection.tsx index 279a644f..3676caee 100644 --- a/frontend/src/components/admin/HoursSection.tsx +++ b/frontend/src/components/admin/HoursSection.tsx @@ -358,6 +358,7 @@ export const HoursSection: React.FC = ({ className="mt-3" label={t('customers.hours.form.bookToProject', 'Book to project') as string} value={projectId} + customerAccountId={customerId} onChange={setProjectId} />