fix(projects): scope 'book to project' to the current customer

A project has at most one customer, so the hours picker shouldn't offer
other customers' projects. HoursSection now passes customerAccountId to
ProjectSelect (shows this customer's projects + still-unassigned ones).
Backend createEntry rejects a projectId owned by a different customer
(422 PROJECT_CUSTOMER_MISMATCH) as defence-in-depth behind the picker.
This commit is contained in:
Luca
2026-06-07 00:58:44 +02:00
parent 0cc52f3693
commit 89bfb6c519
2 changed files with 14 additions and 2 deletions
+13 -2
View File
@@ -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];
@@ -358,6 +358,7 @@ export const HoursSection: React.FC<HoursSectionProps> = ({
className="mt-3"
label={t('customers.hours.form.bookToProject', 'Book to project') as string}
value={projectId}
customerAccountId={customerId}
onChange={setProjectId}
/>
<div className="mt-3 flex items-center justify-end gap-3">