fix: security, validation, and data integrity fixes across 53 files
- Auth: HS256 algorithm restriction on JWT verify, timing-safe bcrypt
for inactive/locked users, locked_until check in loadAuthData, TOTP
fixes (async bcrypt, BigInt conversion, future-code counter fix)
- Validation: Zod enums for leave_type/status, numeric transforms on
foreign keys, VAT 0% coercion fix (Number(v)||21 → v!=null checks)
- Permissions: requirePermission on attendance PUT, attendance_users
and project_logs access checks, trips users filtered by trips.record
- Prisma queries: fixed roles.is:{OR} pattern (doesn't work on to-one
relations), attendance_users now filters by attendance.record only
- Transactions: wrapped deleteOrder, createOrder, updateUser, deleteUser,
duplicateOffer, bulkCreateAttendance, createLeave, scope-templates,
leave-requests, company-settings, profile updates
- Frontend: mountedRef reset in useListData, blob URL cleanup on unmount,
null checks on date fields, AdminDatePicker min/max for HH:mm
- Security headers: COOP, CORP, CSP frame-ancestors/form-action/base-uri
- Other: exchange-rate cache TTL, invoice-alert midnight comparison fix,
numbering.service releaseSequence no-op, nas-offers filename sanitize,
Content-Disposition header injection fix, mojibake Czech strings
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,9 @@ function enrichOrder(o: any) {
|
||||
0,
|
||||
);
|
||||
const vatAmount = o.apply_vat
|
||||
? subtotal * ((Number(o.vat_rate) || 21) / 100)
|
||||
? subtotal *
|
||||
((o.vat_rate != null && o.vat_rate !== "" ? Number(o.vat_rate) : 21) /
|
||||
100)
|
||||
: 0;
|
||||
const { order_items, order_sections, ...rest } = o;
|
||||
const invoice = o.invoices?.[0] || null;
|
||||
@@ -126,7 +128,7 @@ export async function getOrder(id: number) {
|
||||
},
|
||||
invoices: {
|
||||
select: { id: true, invoice_number: true, status: true },
|
||||
take: 1,
|
||||
orderBy: { id: "desc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -290,66 +292,78 @@ interface CreateOrderData {
|
||||
}
|
||||
|
||||
export async function createOrder(body: CreateOrderData) {
|
||||
const orderNumber =
|
||||
body.order_number !== undefined && body.order_number !== null
|
||||
? String(body.order_number)
|
||||
: await generateSharedNumber();
|
||||
try {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const orderNumber =
|
||||
body.order_number !== undefined && body.order_number !== null
|
||||
? String(body.order_number)
|
||||
: await generateSharedNumber(tx);
|
||||
|
||||
if (body.order_number !== undefined && body.order_number !== null) {
|
||||
const taken = await isOrderNumberTaken(String(body.order_number));
|
||||
if (taken) {
|
||||
return { error: "Číslo objednávky je již použito", status: 400 } as const;
|
||||
}
|
||||
}
|
||||
if (body.order_number !== undefined && body.order_number !== null) {
|
||||
const taken = await isOrderNumberTaken(String(body.order_number));
|
||||
if (taken) {
|
||||
throw Object.assign(new Error("Číslo objednávky je již použito"), {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const order = await tx.orders.create({
|
||||
data: {
|
||||
order_number: orderNumber,
|
||||
customer_order_number: body.customer_order_number ?? null,
|
||||
quotation_id: body.quotation_id ?? null,
|
||||
customer_id: body.customer_id ?? null,
|
||||
status: body.status,
|
||||
currency: body.currency,
|
||||
language: body.language,
|
||||
vat_rate: body.vat_rate,
|
||||
apply_vat: body.apply_vat !== false,
|
||||
exchange_rate: body.exchange_rate,
|
||||
scope_title: body.scope_title ?? null,
|
||||
scope_description: body.scope_description ?? null,
|
||||
notes: body.notes ?? null,
|
||||
},
|
||||
const order = await tx.orders.create({
|
||||
data: {
|
||||
order_number: orderNumber,
|
||||
customer_order_number: body.customer_order_number ?? null,
|
||||
quotation_id: body.quotation_id ?? null,
|
||||
customer_id: body.customer_id ?? null,
|
||||
status: body.status,
|
||||
currency: body.currency,
|
||||
language: body.language,
|
||||
vat_rate: body.vat_rate,
|
||||
apply_vat: body.apply_vat !== false,
|
||||
exchange_rate: body.exchange_rate,
|
||||
scope_title: body.scope_title ?? null,
|
||||
scope_description: body.scope_description ?? null,
|
||||
notes: body.notes ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
if (Array.isArray(body.items)) {
|
||||
await tx.order_items.createMany({
|
||||
data: body.items.map((item, i) => ({
|
||||
order_id: order.id,
|
||||
description: item.description ?? null,
|
||||
item_description: item.item_description ?? null,
|
||||
quantity: item.quantity ?? 1,
|
||||
unit: item.unit ?? null,
|
||||
unit_price: item.unit_price ?? 0,
|
||||
is_included_in_total: item.is_included_in_total !== false,
|
||||
position: item.position ?? i,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(body.sections)) {
|
||||
await tx.order_sections.createMany({
|
||||
data: body.sections.map((s, i) => ({
|
||||
order_id: order.id,
|
||||
title: s.title ?? null,
|
||||
title_cz: s.title_cz ?? null,
|
||||
content: s.content ?? null,
|
||||
position: s.position ?? i,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return { id: order.id, order_number: order.order_number };
|
||||
});
|
||||
|
||||
if (Array.isArray(body.items)) {
|
||||
await tx.order_items.createMany({
|
||||
data: body.items.map((item, i) => ({
|
||||
order_id: order.id,
|
||||
description: item.description ?? null,
|
||||
item_description: item.item_description ?? null,
|
||||
quantity: item.quantity ?? 1,
|
||||
unit: item.unit ?? null,
|
||||
unit_price: item.unit_price ?? 0,
|
||||
is_included_in_total: item.is_included_in_total !== false,
|
||||
position: item.position ?? i,
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof Error && "status" in err) {
|
||||
return {
|
||||
error: err.message,
|
||||
status: (err as Error & { status: number }).status,
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(body.sections)) {
|
||||
await tx.order_sections.createMany({
|
||||
data: body.sections.map((s, i) => ({
|
||||
order_id: order.id,
|
||||
title: s.title ?? null,
|
||||
title_cz: s.title_cz ?? null,
|
||||
content: s.content ?? null,
|
||||
position: s.position ?? i,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return { id: order.id, order_number: order.order_number };
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateOrderData {
|
||||
@@ -499,30 +513,34 @@ export async function deleteOrder(id: number) {
|
||||
if (!existing)
|
||||
return { error: "Objednávka nenalezena", status: 404 } as const;
|
||||
|
||||
// Clear quotation back-reference (matching PHP)
|
||||
await prisma.quotations.updateMany({
|
||||
where: { order_id: id },
|
||||
data: { order_id: null },
|
||||
});
|
||||
|
||||
// Delete linked project and its notes (matching PHP)
|
||||
// Fetch linked projects before the transaction for number release later
|
||||
const linkedProjects = await prisma.projects.findMany({
|
||||
where: { order_id: id },
|
||||
select: { id: true, created_at: true },
|
||||
});
|
||||
if (linkedProjects.length > 0) {
|
||||
const projectIds = linkedProjects.map((p) => p.id);
|
||||
await prisma.project_notes.deleteMany({
|
||||
where: { project_id: { in: projectIds } },
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Clear quotation back-reference (matching PHP)
|
||||
await tx.quotations.updateMany({
|
||||
where: { order_id: id },
|
||||
data: { order_id: null },
|
||||
});
|
||||
await prisma.projects.deleteMany({ where: { order_id: id } });
|
||||
}
|
||||
|
||||
// Explicitly clean up child rows
|
||||
await prisma.order_items.deleteMany({ where: { order_id: id } });
|
||||
await prisma.order_sections.deleteMany({ where: { order_id: id } });
|
||||
// Delete linked project and its notes (matching PHP)
|
||||
if (linkedProjects.length > 0) {
|
||||
const projectIds = linkedProjects.map((p) => p.id);
|
||||
await tx.project_notes.deleteMany({
|
||||
where: { project_id: { in: projectIds } },
|
||||
});
|
||||
await tx.projects.deleteMany({ where: { order_id: id } });
|
||||
}
|
||||
|
||||
await prisma.orders.delete({ where: { id } });
|
||||
// Explicitly clean up child rows
|
||||
await tx.order_items.deleteMany({ where: { order_id: id } });
|
||||
await tx.order_sections.deleteMany({ where: { order_id: id } });
|
||||
|
||||
await tx.orders.delete({ where: { id } });
|
||||
});
|
||||
|
||||
const releasedYears = new Set<number>();
|
||||
const year = existing.created_at
|
||||
|
||||
Reference in New Issue
Block a user