security: fix all Medium findings from FLAWS_REPORT audit

- Auth: TOTP replay protection with counter tracking, constant-time
  backup code comparison, atomic lockout increment, per-token logout
- Invoices/PDFs: net-based VAT calculation, dangerous URL scheme
  stripping in cleanQuillHtml, orders-pdf error handling
- Orders: reject item changes on status transition, cascading
  delete cleanup, take:1 with orderBy
- Projects: atomic rename collision handling, MIME/extension
  validation, empty customer name rejection
- Attendance: Czech public holiday awareness in frontend fund
  calculation, leave_hours 0 handling, invalid date NaN guard,
  bounded per-month queries in workfund
- Users/Admin: profile audit logging + password validation, session
  revocation guard, session ID validation, dashboard DB aggregation,
  soft-deleted record protection in scope templates
- Frontend: FormField label linkage, Pagination ARIA, error
  handling in OrderConfirmationModal, 401 propagation, GPS emoji
  hidden from screen readers, table sort state fix, geolocation
  race/abort cleanup, Leaflet popup DOM safety, Vehicles toggleActive
  minimal body, CompanySettings ref mutation fix, OfferDetail unlock
  abort, AttendanceBalances combined fetches
- Utils: env validation, Puppeteer concurrency mutex, invoice alert
  cron cleanup on shutdown, body limit alignment, TOTP error logging,
  trustProxy from env, symlink rejection, rate cache Map usage

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-04-24 08:24:14 +02:00
parent 528e55991b
commit 4f4b12f039
33 changed files with 442 additions and 211 deletions

View File

@@ -97,7 +97,11 @@ export async function listOrders(params: ListOrdersParams) {
order_items: { orderBy: { position: "asc" } },
order_sections: { orderBy: { position: "asc" } },
quotations: { select: { quotation_number: true, project_code: true } },
invoices: { select: { id: true, invoice_number: true }, take: 1 },
invoices: {
select: { id: true, invoice_number: true },
take: 1,
orderBy: { id: "desc" },
},
},
}),
prisma.orders.count({ where }),
@@ -410,6 +414,16 @@ export async function updateOrder(id: number, body: UpdateOrderData) {
status: 400,
} as const;
}
if (
body.status !== undefined &&
(String(body.status) === "dokoncena" ||
String(body.status) === "stornovana")
) {
return {
error: "Nelze upravit položky při změně stavu na dokončeno/storno",
status: 400,
} as const;
}
await prisma.$transaction(async (tx) => {
await tx.orders.update({ where: { id }, data });
@@ -504,6 +518,10 @@ export async function deleteOrder(id: number) {
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 } });
await prisma.orders.delete({ where: { id } });
const releasedYears = new Set<number>();