# 2026-06-10 — Audit fix pass #1: all verified CRITICAL/HIGH findings Source: `REVIEW_FINDINGS.md` (2026-06-09 full audit, 304 files). Scope of this pass: the 19 verified per-file **HIGH** findings + the project-level **CRITICAL** dependency finding. The two project-level HIGH _structural refactors_ (service-layer extraction for 6 route files; PDF template dedup) are explicitly **out of scope** — they are multi-thousand-line refactors that deserve their own pass. Execution: subagent-driven development — one implementer per task (sequential, shared test DB forbids parallel test runs), spec-compliance review then code-quality review after each. No commits until the user asks. Final full gates: `npx tsc -b --noEmit` · `npx vitest run` · `npm run lint` · `npm run build`. --- ## Task H — Dependencies (project-level CRITICAL) — done inline by controller - Remove `concurrently` from devDependencies (unused; carries both critical advisories GHSA-w7jw-789q-3m8p / shell-quote CVSS 8.1). - Add `"overrides": { "@hono/node-server": "^1.19.13" }` (clears the 3 moderate advisories from the prisma → @prisma/dev chain; do NOT take npm's Prisma 6 downgrade). - Remove deprecated stub types `@types/dompurify`, `@types/bcryptjs`; move `@types/jsdom` to devDependencies. - Add `qrcode` + `@types/qrcode` (needed by Task B to replace the CSP-blocked external QR service). - `npm install`, verify `npm audit` (criticals+moderates gone), typecheck. ## Task A — Attendance create/edit broken since 519edce (top-5 #1) Findings (both verified HIGH): - `src/admin/pages/AttendanceCreate.tsx:80-99` — handleSubmit posts the raw form whose shape the API does not accept: `arrival_date`/`break_start_date`/`break_start_time`/ `break_end_*`/`departure_date` are not in CreateAttendanceSchema (silently stripped — breaks and overnight dates never save); times are never combined with dates (unlike useAttendanceAdmin's combineDatetime), so a validated "HH:MM" would reach createAttendance which does `new Date('08:00')` → Invalid Date → 500; and since commit 519edce the always-sent empty-string time fields (`arrival_time: ""`) fail timeString validation, so EVERY submit from this page — including leave records — returns 400. - `src/admin/pages/AttendanceAdmin.tsx:407-442` (logic in `useAttendanceAdmin.ts`) — create/edit modal submits send combined `YYYY-MM-DDTHH:MM:00` datetimes for arrival/departure/break, but since 519edce CreateAttendanceSchema/UpdateAttendanceSchema validate them with `timeString` (HH:MM only) — every create/edit containing a time is rejected 400 "Čas musí být ve formátu HH:MM" (and the service itself expects full datetimes via `new Date(...)`); additionally `break_start`/`break_end` are absent from CreateAttendanceSchema, so breaks are silently stripped on create. Direction: first `git show 519edce` to understand the intent; the schema contradicts both client and service, so the fix is to make the schemas validate the combined local-datetime wire format the client sends and the service consumes (lenient helper in `src/schemas/common.ts` per repo convention), add `break_start`/`break_end` to the create schema, and rewrite AttendanceCreate.tsx's submit to combine date+time and omit empty optionals. TDD: route-level regression tests (work record with times+breaks, overnight, leave record without times, update) in `src/__tests__/`. ## Task B — 2FA: backup-code login, enrollment QR, dashboard status (top-5 #2) Findings (all verified HIGH): - `src/admin/context/AuthContext.tsx:263-272` — backup-code login broken: verify2FA always POSTs to /login/totp with the code as totp_code plus an isBackup flag the server ignores; TotpVerifySchema requires exactly 6 digits so 8-char backup codes always 400. The real endpoint POST /api/admin/totp/backup-verify (expects backup_code) is never called anywhere in the frontend — lockout for any user who lost their authenticator. - `src/admin/components/dashboard/DashProfile.tsx:556` — QR `` from api.qrserver.com is blocked by the production CSP (img-src 'self' data: blob: + osm tiles), so prod 2FA enrollment shows a broken image. (Also a privacy bug: the TOTP secret is sent to a third-party URL.) Fix: generate the QR locally with the `qrcode` package (`QRCode.toDataURL(otpauthUrl)`) — data: is already CSP-allowed. - `src/admin/pages/Dashboard.tsx:89-91` — totpEnabled derived from require2FAOptions() which returns the COMPANY-WIDE require_2fa policy flag, not the user's enrollment; the per-user endpoint (totp.ts:190-197) is never queried. Wrong "2FA Aktivní/Neaktivní" and wrong button shown. Direction: read `src/routes/admin/totp.ts` first; wire Login/AuthContext so a backup code goes to backup-verify and completes the login-token flow. If the server endpoint doesn't fit the loginToken flow, extend it minimally (single-use code consumption + logAudit preserved). Server test for backup-verify login path if server is touched. ## Task C — Invoice/issued-order edit integrity (top-5 #3) Findings (all verified HIGH): - `src/admin/pages/InvoiceDetail.tsx:728` — edit hydration overwrites each item's persisted per-line VAT with the invoice-level rate (`vat_rate: Number(inv.vat_rate) || 21`) though invoice_items.vat_rate is a real per-item column — re-saving a mixed-rate invoice silently corrupts per-line VAT in the DB; `|| 21` also turns legitimate 0% into 21% (same falsy-zero at line 689). - `src/admin/pages/InvoiceDetail.tsx:1891-1902` — editing "Text fakturace (na PDF)" is silently lost: payload includes billing_text but UpdateInvoiceSchema (src/schemas/invoices.schema.ts:46-67) has no billing_text field, so Zod strips it (updateInvoice supports it via strFields). Add the field to the schema + server test. - `src/admin/pages/IssuedOrderDetail.tsx:606-611` — hydration falsy-fallbacks: `quantity: Number(it.quantity) || 1` turns saved 0 into 1; `vat_rate: Number(it.vat_rate) || Number(d.vat_rate) || 21` turns a saved 0% line into 21% — displayed wrong and silently persisted on next save. - `src/admin/pages/IssuedOrderDetail.tsx:826-839` — handleStatusChange never mirrors the new status into form.status, so StatusChip, the `editable` flag and the delete-button gate use the stale status after every transition. Use Number.isFinite-style coercion that respects 0 (e.g. `const n = Number(x); Number.isFinite(n) ? n : fallback`). ## Task D — Trips: truncated lists/totals + dropped vehicle edit (top-5 #4a) Findings (all verified HIGH): - `src/admin/lib/queries/trips.ts:52-63` — tripListOptions consumes the paginated endpoint via plain jsonQuery, discarding pagination meta; Trips.tsx uses default limit 25 and TripsAdmin perPage:100 (server hard cap) — both silently truncate with no pagination controls. - `src/admin/lib/queries/trips.ts:95-103` — tripHistoryOptions sends no page/per_page → 25 rows; TripsHistory.tsx computes monthly km/business totals from the truncated rows. - `src/admin/pages/Trips.tsx:324-336` — stat cards computed from the 25-row page; header labels stats "current month" though the query has no month filter. - `src/admin/pages/TripsAdmin.tsx:295,665-676` — edit modal sends vehicle_id but UpdateTripSchema (src/schemas/trips.schema.ts:23-31) has no vehicle_id and the PUT handler never applies it — vehicle change silently dropped. Add to schema (nullableIntIdFromForm) + apply in route + audit + server test. - `src/admin/pages/TripsHistory.tsx:98-104,121-128` — same 25-row truncation for the month view and its stat cards. Direction for totals: prefer a server-side aggregate (the repo already has per-currency totals endpoints for invoices/orders as the pattern) over fetching all pages client-side; lists get real pagination using the existing `usePaginatedQuery`/`Pagination` kit pieces used elsewhere. Keep the month-filter semantics of TripsHistory correct (totals must cover the WHOLE month, not one page). ## Task E — orders.service.ts: PDF blob in every response (top-5 #4b) Finding (verified HIGH): `src/services/orders.service.ts:154-176,196-216,228-263,570,683` — attachment_data (full PDF blob, Bytes) is fetched on every query with no select/omit and spread into API responses by enrichOrder/getOrder; getOrderTotals loads every blob for the whole filtered set just to sum totals; updateOrder/deleteOrder pre-reads pull the blob to check status/number. A dedicated /:id/attachment endpoint already exists. Exclude the blob from list/detail/totals/pre-reads (keep has_attachment-style boolean if the FE needs it — check FE usage), keep the attachment endpoint working, extend `src/__tests__/orders-list.test.ts` to assert attachment_data is absent. ## Task F — Warehouse: unit enum 400s + dead attachment download Findings (both verified HIGH): - `src/admin/pages/WarehouseItemDetail.tsx:451-462` — "Jednotka" is free-text but the server requires UnitEnum ("ks","m","kg","bal","sada","m2","m3","l") — any other value 400s create/update. Make it a Select over the enum values (single source: mirror the server enum list). - `src/admin/pages/WarehouseReceiptDetail.tsx:251-259` — attachment link targets GET /warehouse/receipts/:id/attachments/:attachmentId which does not exist (only POST upload + DELETE are registered in warehouse.ts), and a plain sends no Authorization header anyway. Add the GET download route (requirePermission, correct content-type/disposition) + authenticated blob download in the UI (fetch via apiFetch → object URL, like other binary downloads in the repo). Server test for the new route. ## Task G — Small fixes - `.claude/hooks/block-env.js:10-11` (verified HIGH) — block decision never takes effect: prints {decision:'block'} JSON to stdout then exits 1; Claude Code only parses stdout JSON on exit 0 and only blocks on exit 2 (reason on stderr). Fix: exit 2 with the reason on stderr. - `src/admin/pages/Projects.tsx:153-154` (verified HIGH) — create modal submits start_date/end_date initialized to "" which isoDateString.nullish() rejects → creating a project without filling BOTH dates always 400s. Send null/omit when empty. --- ## Status - [x] H — dependencies (controller, inline) — criticals+moderates cleared; only the 2 known-mitigated quill lows remain - [x] A — attendance create/edit — dateTimeString helpers, break fields, rewritten AttendanceCreate payload; 6 regression tests - [x] B — 2FA — backup-verify wired (+ remember-me parity server-side), local QR via qrcode lib, per-user totp status; 6 tests - [x] C — invoice/issued-order edit integrity — numberOr (shared in formatters.ts), billing_text in UpdateInvoiceSchema, status mirror; 2 tests - [x] D — trips — paginated queries + Pagination UI on 3 pages, GET /trips/stats (buildTripsWhere shared), vehicle_id on PUT + both-vehicle odometer recompute, print rebuilt (sync window.open, escaped string template); 7 tests - [x] E — orders attachment_data excluded from all non-binary reads (incl. numbering/invoices/orders-pdf callers); 4 tests - [x] F — warehouse unit Select + GET receipt attachment route + authenticated blob download; RFC 5987 content-disposition helper (also fixes received-invoices + orders Czech-filename 500); 6 tests - [x] G — block-env hook exit 2/stderr (verified empirically), Projects empty dates → null - [x] Final gates: tsc -b clean · vitest 30 files / 342 tests green · lint 0 errors · build OK. Whole-diff 3-lens review: see final report.