security: fix all Critical and High findings from FLAWS_REPORT audit

- Auth: pessimistic locking on login tokens and refresh token rotation,
  backup code attempt counter, rate limiting verification
- Schema: unique constraints on business numbers, FK relations,
  unsigned/signed alignment, attendance duplicate prevention
- Invoices/PDFs: DOMPurify sanitization, bounded queries in stats
  and alerts, VAT rounding, Puppeteer error handling
- Orders/Offers: transactional parent+child creation, Zod NaN
  refinement, status enums, uniqueness checks
- Projects/Files: path traversal protection, streamed uploads,
  permission guards, query param validation
- Attendance/HR: duplicate checks, ownership validation, GPS
  restrictions, trip distance validation
- Frontend: modal lock reference counting, XSS escaping in print
  HTML, ref mutation fixes, accessibility attributes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-04-24 00:58:35 +02:00
parent 122eee175e
commit 528e55991b
57 changed files with 2355 additions and 1010 deletions

View File

@@ -3,6 +3,7 @@ import {
generateSharedNumber,
previewSharedNumber,
releaseSharedNumber,
isOrderNumberTaken,
} from "./numbering.service";
interface OrderItemInput {
@@ -290,52 +291,61 @@ export async function createOrder(body: CreateOrderData) {
? String(body.order_number)
: await generateSharedNumber();
const order = await prisma.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 (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;
}
}
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,
},
});
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 prisma.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 prisma.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 };
}
interface UpdateOrderData {
@@ -393,24 +403,6 @@ export async function updateOrder(id: number, body: UpdateOrderData) {
data.apply_vat =
body.apply_vat === true || body.apply_vat === 1 || body.apply_vat === "1";
await prisma.orders.update({ where: { id }, data });
// Sync project status when order status changes (matching PHP)
if (body.status !== undefined && String(body.status) !== currentStatus) {
const statusMap: Record<string, string> = {
v_realizaci: "aktivni",
dokoncena: "dokonceny",
stornovana: "zruseny",
};
const projectStatus = statusMap[String(body.status)];
if (projectStatus) {
await prisma.projects.updateMany({
where: { order_id: id },
data: { status: projectStatus },
});
}
}
if (Array.isArray(body.items) || Array.isArray(body.sections)) {
if (currentStatus !== "prijata" && currentStatus !== "v_realizaci") {
return {
@@ -419,6 +411,24 @@ export async function updateOrder(id: number, body: UpdateOrderData) {
} as const;
}
await prisma.$transaction(async (tx) => {
await tx.orders.update({ where: { id }, data });
// Sync project status when order status changes (matching PHP)
if (body.status !== undefined && String(body.status) !== currentStatus) {
const statusMap: Record<string, string> = {
v_realizaci: "aktivni",
dokoncena: "dokonceny",
stornovana: "zruseny",
};
const projectStatus = statusMap[String(body.status)];
if (projectStatus) {
await tx.projects.updateMany({
where: { order_id: id },
data: { status: projectStatus },
});
}
}
if (Array.isArray(body.items)) {
await tx.order_items.deleteMany({ where: { order_id: id } });
await tx.order_items.createMany({
@@ -447,6 +457,24 @@ export async function updateOrder(id: number, body: UpdateOrderData) {
});
}
});
} else {
await prisma.orders.update({ where: { id }, data });
// Sync project status when order status changes (matching PHP)
if (body.status !== undefined && String(body.status) !== currentStatus) {
const statusMap: Record<string, string> = {
v_realizaci: "aktivni",
dokoncena: "dokonceny",
stornovana: "zruseny",
};
const projectStatus = statusMap[String(body.status)];
if (projectStatus) {
await prisma.projects.updateMany({
where: { order_id: id },
data: { status: projectStatus },
});
}
}
}
return { data: { id, order_number: existing.order_number } };
@@ -478,17 +506,22 @@ export async function deleteOrder(id: number) {
await prisma.orders.delete({ where: { id } });
const releasedYears = new Set<number>();
const year = existing.created_at
? new Date(existing.created_at).getFullYear()
: new Date().getFullYear();
await releaseSharedNumber(year);
releasedYears.add(year);
// Release the linked project's shared number(s) too
for (const p of linkedProjects) {
const pYear = p.created_at
? new Date(p.created_at).getFullYear()
: new Date().getFullYear();
await releaseSharedNumber(pYear);
if (!releasedYears.has(pYear)) {
await releaseSharedNumber(pYear);
releasedYears.add(pYear);
}
}
return { data: { id, order_number: existing.order_number } };