fix: audit fix pass #1 — all 19 verified HIGH findings + critical dep cleanup

Fixes every CRITICAL/HIGH finding from the 2026-06-09 full-codebase audit
(REVIEW_FINDINGS.md); each fix went through independent spec + code-quality
review. Plan and per-task log: docs/superpowers/plans/2026-06-10-audit-high-fix-pass.md

- attendance: schemas accept the combined local datetimes the forms/service
  use (new dateTimeString helpers in schemas/common.ts), breaks persist on
  create, AttendanceCreate submit rebuilt — every submit 400'd since 519edce
- 2fa: backup codes wired to /totp/backup-verify (+ remember-me parity),
  enrollment QR generated locally via qrcode (CSP-blocked external service
  also leaked the secret), dashboard shows per-user enrollment, not policy
- invoices/orders: per-line VAT survives re-saves (numberOr 0-respecting
  coercion in formatters.ts), billing_text persists on update, issued-order
  status transitions update UI gates
- trips: real pagination on all 3 pages, GET /trips/stats server aggregate
  (shared buildTripsWhere + legacy distance coalesce), vehicle_id applies on
  PUT with both-vehicle odometer recompute, print rebuilt (sync window.open,
  escaped template, server totals)
- orders api: attachment_data PDF blob excluded from all non-binary reads
- warehouse: unit field is a Select over UnitEnum, receipt attachments
  downloadable via new authenticated GET route
- downloads: shared RFC 5987 contentDisposition helper — Czech filenames no
  longer 500 (warehouse, received-invoices, orders endpoints)
- misc: block-env hook actually blocks (exit 2 + stderr), project create
  works with empty dates, NaN filter guards on trips endpoints
- deps: remove unused concurrently (clears both critical advisories), pin
  @hono/node-server >=1.19.13 via overrides (clears the 3 moderates without
  the Prisma 6 downgrade), drop deprecated @types stubs

Gates: tsc -b clean - vitest 30 files / 342 tests (31 new) - eslint 0 errors
- build OK

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-10 09:59:47 +02:00
parent d08e55a41a
commit 1826fc7976
44 changed files with 2812 additions and 622 deletions

View File

@@ -126,6 +126,8 @@ export interface CreateAttendanceData {
departure_lng?: number | null;
departure_accuracy?: number | null;
departure_address?: string | null;
break_start?: string | null;
break_end?: string | null;
notes?: string | null;
project_id?: number | null;
leave_type: string;
@@ -1608,6 +1610,8 @@ export async function createAttendance(
departure_lng: data.departure_lng ?? null,
departure_accuracy: data.departure_accuracy ?? null,
departure_address: data.departure_address ?? null,
break_start: data.break_start ? new Date(data.break_start) : null,
break_end: data.break_end ? new Date(data.break_end) : null,
notes: data.notes ?? null,
project_id: data.project_id ?? null,
leave_type: data.leave_type as attendance_leave_type,

View File

@@ -430,6 +430,9 @@ export async function getInvoiceStats(queryMonth?: number, queryYear?: number) {
export async function getOrderDataForInvoice(orderId: number) {
const order = await prisma.orders.findUnique({
where: { id: orderId },
// This result is spread into the order-data API response — never include
// the PO attachment blob (served only by GET /orders/:id/attachment).
omit: { attachment_data: true },
include: {
customers: true,
order_items: { orderBy: { position: "asc" } },

View File

@@ -206,8 +206,16 @@ async function releaseSequence(
/** Verify a shared number is not already used by an order or project. */
export async function isSharedNumberTaken(number: string): Promise<boolean> {
const [existingOrder, existingProject] = await Promise.all([
prisma.orders.findFirst({ where: { order_number: number } }),
prisma.projects.findFirst({ where: { project_number: number } }),
// select id only — the orders row carries the PO attachment blob, which an
// existence check must never pull.
prisma.orders.findFirst({
where: { order_number: number },
select: { id: true },
}),
prisma.projects.findFirst({
where: { project_number: number },
select: { id: true },
}),
]);
return !!(existingOrder || existingProject);
}
@@ -238,8 +246,11 @@ export async function isOfferNumberTaken(number: string): Promise<boolean> {
/** Verify an order number is not already used. */
export async function isOrderNumberTaken(number: string): Promise<boolean> {
// select id only — the orders row carries the PO attachment blob, which an
// existence check must never pull.
const existing = await prisma.orders.findFirst({
where: { order_number: number },
select: { id: true },
});
return !!existing;
}

View File

@@ -72,6 +72,10 @@ async function syncProjectStatus(
});
}
// ⚠ Also called by getOrderTotals with a MINIMAL select (currency, apply_vat,
// vat_rate, item quantity/unit_price/is_included_in_total). If you read a NEW
// order/item field here, add it to that select — a field missing from the
// select is silently 0 in the per-currency totals.
function enrichOrder(o: any) {
const subtotal = o.order_items
.filter((i: any) => i.is_included_in_total !== false)
@@ -158,6 +162,11 @@ export async function listOrders(params: ListOrdersParams) {
take: limit,
// Tiebreak on id so same-second created_at rows sort deterministically.
orderBy: [{ [sortField]: order }, { id: order }],
// The PO attachment blob is served ONLY by GET /:id/attachment. Never
// pull it into list payloads — a single 1MB PDF would serialize into
// megabytes of JSON per row. attachment_name stays as the existence
// signal the frontend uses.
omit: { attachment_data: true },
include: {
customers: { select: { id: true, name: true } },
order_items: { orderBy: { position: "asc" } },
@@ -193,17 +202,21 @@ export async function getOrderTotals(
): Promise<{ totals: CurrencyAmount[] }> {
const where = buildOrderWhere(params);
// Select ONLY what the math needs (currency + VAT flags + item numbers).
// This runs over the WHOLE filtered set, so pulling attachment blobs,
// sections HTML or relations here would multiply the query size for nothing.
const orders = await prisma.orders.findMany({
where,
include: {
customers: { select: { id: true, name: true } },
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,
orderBy: { id: "desc" },
select: {
currency: true,
apply_vat: true,
vat_rate: true,
order_items: {
select: {
quantity: true,
unit_price: true,
is_included_in_total: true,
},
},
},
});
@@ -228,6 +241,9 @@ export async function getOrderTotals(
export async function getOrder(id: number) {
const order = await prisma.orders.findUnique({
where: { id },
// The blob belongs only to GET /:id/attachment — the detail payload
// carries attachment_name as the existence signal.
omit: { attachment_data: true },
include: {
customers: true,
order_items: { orderBy: { position: "asc" } },
@@ -567,7 +583,11 @@ interface UpdateOrderData {
}
export async function updateOrder(id: number, body: UpdateOrderData) {
const existing = await prisma.orders.findUnique({ where: { id } });
// Pre-read only the fields the guards below check — never the PDF blob.
const existing = await prisma.orders.findUnique({
where: { id },
select: { status: true, order_number: true },
});
if (!existing)
return { error: "Objednávka nenalezena", status: 404 } as const;
@@ -680,7 +700,11 @@ export async function updateOrder(id: number, body: UpdateOrderData) {
}
export async function deleteOrder(id: number, deleteFiles = false) {
const existing = await prisma.orders.findUnique({ where: { id } });
// Pre-read only what the delete needs (number release + year) — not the blob.
const existing = await prisma.orders.findUnique({
where: { id },
select: { order_number: true, created_at: true },
});
if (!existing)
return { error: "Objednávka nenalezena", status: 404 } as const;