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 {
generateOfferNumber,
previewOfferNumber,
releaseOfferNumber,
isOfferNumberTaken,
} from "./numbering.service";
interface QuotationItemInput {
@@ -142,53 +143,64 @@ export async function createOffer(body: Record<string, any>) {
? String(body.quotation_number)
: await generateOfferNumber();
const quotation = await prisma.quotations.create({
data: {
quotation_number: quotationNumber,
project_code: body.project_code ? String(body.project_code) : null,
customer_id: body.customer_id ? Number(body.customer_id) : null,
valid_until: body.valid_until ? new Date(String(body.valid_until)) : null,
currency: body.currency ? String(body.currency) : "CZK",
language: body.language ? String(body.language) : "cs",
vat_rate: body.vat_rate ? Number(body.vat_rate) : 21.0,
apply_vat: body.apply_vat !== false,
exchange_rate: body.exchange_rate ? Number(body.exchange_rate) : 1.0,
status: body.status ? String(body.status) : "active",
scope_title: body.scope_title ? String(body.scope_title) : null,
scope_description: body.scope_description
? String(body.scope_description)
: null,
},
if (body.quotation_number !== undefined && body.quotation_number !== null) {
const taken = await isOfferNumberTaken(String(body.quotation_number));
if (taken) {
return { error: "Číslo nabídky je již použito", status: 400 } as const;
}
}
return prisma.$transaction(async (tx) => {
const quotation = await tx.quotations.create({
data: {
quotation_number: quotationNumber,
project_code: body.project_code ? String(body.project_code) : null,
customer_id: body.customer_id ? Number(body.customer_id) : null,
valid_until: body.valid_until
? new Date(String(body.valid_until))
: null,
currency: body.currency ? String(body.currency) : "CZK",
language: body.language ? String(body.language) : "cs",
vat_rate: body.vat_rate ? Number(body.vat_rate) : 21.0,
apply_vat: body.apply_vat !== false,
exchange_rate: body.exchange_rate ? Number(body.exchange_rate) : 1.0,
status: body.status ? String(body.status) : "active",
scope_title: body.scope_title ? String(body.scope_title) : null,
scope_description: body.scope_description
? String(body.scope_description)
: null,
},
});
if (Array.isArray(body.items)) {
await tx.quotation_items.createMany({
data: (body.items as QuotationItemInput[]).map((item, i) => ({
quotation_id: quotation.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.scope_sections.createMany({
data: (body.sections as ScopeSectionInput[]).map((s, i) => ({
quotation_id: quotation.id,
title: s.title ?? null,
title_cz: s.title_cz ?? null,
content: s.content ?? null,
position: s.position ?? i,
})),
});
}
return quotation;
});
if (Array.isArray(body.items)) {
await prisma.quotation_items.createMany({
data: (body.items as QuotationItemInput[]).map((item, i) => ({
quotation_id: quotation.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.scope_sections.createMany({
data: (body.sections as ScopeSectionInput[]).map((s, i) => ({
quotation_id: quotation.id,
title: s.title ?? null,
title_cz: s.title_cz ?? null,
content: s.content ?? null,
position: s.position ?? i,
})),
});
}
return quotation;
}
export async function updateOffer(id: number, body: Record<string, any>) {
@@ -204,55 +216,51 @@ export async function updateOffer(id: number, body: Record<string, any>) {
return { error: "Číslo nabídky nelze změnit", status: 400 } as const;
}
await prisma.quotations.update({
where: { id },
data: {
customer_id:
body.customer_id !== undefined ? Number(body.customer_id) : undefined,
valid_until:
body.valid_until !== undefined
? body.valid_until
? new Date(String(body.valid_until))
: null
: undefined,
currency: body.currency !== undefined ? String(body.currency) : undefined,
language: body.language !== undefined ? String(body.language) : undefined,
vat_rate: body.vat_rate !== undefined ? Number(body.vat_rate) : undefined,
apply_vat:
body.apply_vat !== undefined
? body.apply_vat === true ||
body.apply_vat === 1 ||
body.apply_vat === "1"
: undefined,
exchange_rate:
body.exchange_rate !== undefined
? Number(body.exchange_rate)
: undefined,
status: body.status !== undefined ? String(body.status) : undefined,
project_code:
body.project_code !== undefined
? body.project_code
? String(body.project_code)
: null
: undefined,
scope_title:
body.scope_title !== undefined
? body.scope_title
? String(body.scope_title)
: null
: undefined,
scope_description:
body.scope_description !== undefined
? body.scope_description
? String(body.scope_description)
: null
: undefined,
modified_at: new Date(),
},
});
const data = {
customer_id:
body.customer_id !== undefined ? Number(body.customer_id) : undefined,
valid_until:
body.valid_until !== undefined
? body.valid_until
? new Date(String(body.valid_until))
: null
: undefined,
currency: body.currency !== undefined ? String(body.currency) : undefined,
language: body.language !== undefined ? String(body.language) : undefined,
vat_rate: body.vat_rate !== undefined ? Number(body.vat_rate) : undefined,
apply_vat:
body.apply_vat !== undefined
? body.apply_vat === true ||
body.apply_vat === 1 ||
body.apply_vat === "1"
: undefined,
exchange_rate:
body.exchange_rate !== undefined ? Number(body.exchange_rate) : undefined,
status: body.status !== undefined ? String(body.status) : undefined,
project_code:
body.project_code !== undefined
? body.project_code
? String(body.project_code)
: null
: undefined,
scope_title:
body.scope_title !== undefined
? body.scope_title
? String(body.scope_title)
: null
: undefined,
scope_description:
body.scope_description !== undefined
? body.scope_description
? String(body.scope_description)
: null
: undefined,
modified_at: new Date(),
};
if (Array.isArray(body.items) || Array.isArray(body.sections)) {
await prisma.$transaction(async (tx) => {
await tx.quotations.update({ where: { id }, data });
if (Array.isArray(body.items)) {
await tx.quotation_items.deleteMany({ where: { quotation_id: id } });
await tx.quotation_items.createMany({
@@ -281,6 +289,8 @@ export async function updateOffer(id: number, body: Record<string, any>) {
});
}
});
} else {
await prisma.quotations.update({ where: { id }, data });
}
return { id, quotation_number: existing.quotation_number };