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:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user