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:
@@ -55,7 +55,9 @@ function enrichQuotation(q: any) {
|
||||
0,
|
||||
);
|
||||
const vatAmount = q.apply_vat
|
||||
? subtotal * ((Number(q.vat_rate) || 21) / 100)
|
||||
? subtotal *
|
||||
((q.vat_rate != null && q.vat_rate !== "" ? Number(q.vat_rate) : 21) /
|
||||
100)
|
||||
: 0;
|
||||
const { quotation_items, scope_sections, ...rest } = q;
|
||||
return {
|
||||
@@ -138,11 +140,6 @@ export async function getOffer(id: number) {
|
||||
}
|
||||
|
||||
export async function createOffer(body: Record<string, any>) {
|
||||
const quotationNumber =
|
||||
body.quotation_number !== undefined && body.quotation_number !== null
|
||||
? String(body.quotation_number)
|
||||
: await generateOfferNumber();
|
||||
|
||||
if (body.quotation_number !== undefined && body.quotation_number !== null) {
|
||||
const taken = await isOfferNumberTaken(String(body.quotation_number));
|
||||
if (taken) {
|
||||
@@ -151,6 +148,11 @@ export async function createOffer(body: Record<string, any>) {
|
||||
}
|
||||
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const quotationNumber =
|
||||
body.quotation_number !== undefined && body.quotation_number !== null
|
||||
? String(body.quotation_number)
|
||||
: await generateOfferNumber(tx);
|
||||
|
||||
const quotation = await tx.quotations.create({
|
||||
data: {
|
||||
quotation_number: quotationNumber,
|
||||
@@ -161,7 +163,7 @@ export async function createOffer(body: Record<string, any>) {
|
||||
: 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,
|
||||
vat_rate: body.vat_rate != null ? 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",
|
||||
@@ -320,53 +322,55 @@ export async function duplicateOffer(id: number) {
|
||||
});
|
||||
if (!original) return null;
|
||||
|
||||
const nextOfferNumber = await generateOfferNumber();
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const nextOfferNumber = await generateOfferNumber(tx);
|
||||
|
||||
const copy = await prisma.quotations.create({
|
||||
data: {
|
||||
quotation_number: nextOfferNumber,
|
||||
project_code: original.project_code,
|
||||
customer_id: original.customer_id,
|
||||
valid_until: null,
|
||||
currency: original.currency,
|
||||
language: original.language,
|
||||
vat_rate: original.vat_rate,
|
||||
apply_vat: original.apply_vat,
|
||||
exchange_rate: original.exchange_rate,
|
||||
status: "active",
|
||||
scope_title: original.scope_title,
|
||||
scope_description: original.scope_description,
|
||||
},
|
||||
const copy = await tx.quotations.create({
|
||||
data: {
|
||||
quotation_number: nextOfferNumber,
|
||||
project_code: original.project_code,
|
||||
customer_id: original.customer_id,
|
||||
valid_until: null,
|
||||
currency: original.currency,
|
||||
language: original.language,
|
||||
vat_rate: original.vat_rate,
|
||||
apply_vat: original.apply_vat,
|
||||
exchange_rate: original.exchange_rate,
|
||||
status: "active",
|
||||
scope_title: original.scope_title,
|
||||
scope_description: original.scope_description,
|
||||
},
|
||||
});
|
||||
|
||||
if (original.quotation_items.length > 0) {
|
||||
await tx.quotation_items.createMany({
|
||||
data: original.quotation_items.map((item) => ({
|
||||
quotation_id: copy.id,
|
||||
description: item.description,
|
||||
item_description: item.item_description,
|
||||
quantity: item.quantity,
|
||||
unit: item.unit,
|
||||
unit_price: item.unit_price,
|
||||
is_included_in_total: item.is_included_in_total,
|
||||
position: item.position,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (original.scope_sections.length > 0) {
|
||||
await tx.scope_sections.createMany({
|
||||
data: original.scope_sections.map((s) => ({
|
||||
quotation_id: copy.id,
|
||||
title: s.title,
|
||||
title_cz: s.title_cz,
|
||||
content: s.content,
|
||||
position: s.position,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return { copy, original };
|
||||
});
|
||||
|
||||
if (original.quotation_items.length > 0) {
|
||||
await prisma.quotation_items.createMany({
|
||||
data: original.quotation_items.map((item) => ({
|
||||
quotation_id: copy.id,
|
||||
description: item.description,
|
||||
item_description: item.item_description,
|
||||
quantity: item.quantity,
|
||||
unit: item.unit,
|
||||
unit_price: item.unit_price,
|
||||
is_included_in_total: item.is_included_in_total,
|
||||
position: item.position,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (original.scope_sections.length > 0) {
|
||||
await prisma.scope_sections.createMany({
|
||||
data: original.scope_sections.map((s) => ({
|
||||
quotation_id: copy.id,
|
||||
title: s.title,
|
||||
title_cz: s.title_cz,
|
||||
content: s.content,
|
||||
position: s.position,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return { copy, original };
|
||||
}
|
||||
|
||||
export async function invalidateOffer(id: number) {
|
||||
|
||||
Reference in New Issue
Block a user