fix: 2026-06-09 full-codebase audit hardening

Validation: shared NaN-guarded Zod coercion helpers in schemas/common.ts replace
the raw number|string transform idiom across every schema (the root-cause NaN bug
class); emailOrEmpty + lenient isoDateString/timeString.

Security: roles privilege-escalation closed; refresh-token family revocation on
reuse; TOTP uses config params; read endpoints permission-guarded; received-invoices
gross VAT on all paths; orders-pdf custom-items authz.

Concurrency: $queryRaw SELECT...FOR UPDATE locks in ascending-id order (warehouse
confirm/cancel, attendance lockUserRow); uniqueness checks moved into create
transactions (TOCTOU -> 409); deterministic id tiebreak on second-precision
timestamp ordering (plan resolveCell/resolveGrid, warehouse FIFO).

Frontend: Rules-of-Hooks fixed across ~14 pages + PlanCellModal; UTC-date persisted
fields; dashboard invalidation gaps; stale-closure confirm bugs.

Tooling/tests: ESLint flat config (react-hooks/rules-of-hooks = error) + Prettier;
tsconfig.test.json so tsc -b type-checks the tests; removed 3 dead deps; npm audit
fix (8 -> 3). Suite 195 -> 247 (happy-path auth, FIFO oldest-first, flakiness fixes),
isolated on app_test via .env.test with a hard-throw setup guard.

Gates: tsc 0 | build 0 | vitest 247/247 | eslint 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-09 06:45:26 +02:00
parent c454d1a3fc
commit 519edce373
179 changed files with 7179 additions and 2844 deletions

View File

@@ -51,6 +51,7 @@ import { offerCustomersOptions, type Customer } from "../lib/queries/offers";
import { bankAccountsOptions } from "../lib/queries/common";
import { jsonQuery } from "../lib/apiAdapter";
import { formatCurrency, formatDate, todayLocalStr } from "../utils/formatters";
import { normalizeDateStr } from "../utils/attendanceHelpers";
import {
Button,
Card,
@@ -69,6 +70,33 @@ import {
const API_BASE = "/api/admin";
/**
* Add `days` to a base date and return `YYYY-MM-DD` in LOCAL (Prague) time.
* `base` is an optional `YYYY-MM-DD` string; when omitted, "today" (local) is
* used. We build the Date from the local year/month/day parts (so it's a local
* midnight, not the UTC midnight `new Date("YYYY-MM-DD")` would give) and read
* it back with local getters — never via `toISOString()`, which would shift a
* day during the evening Prague window. See utils/formatters.todayLocalStr.
*/
function addDaysLocalStr(days: number, base?: string): string {
let y: number, m: number, d: number;
const parts = base ? /^(\d{4})-(\d{2})-(\d{2})/.exec(base) : null;
if (parts) {
y = Number(parts[1]);
m = Number(parts[2]) - 1;
d = Number(parts[3]);
} else {
const now = new Date();
y = now.getFullYear();
m = now.getMonth();
d = now.getDate();
}
const result = new Date(y, m, d + days);
const mm = String(result.getMonth() + 1).padStart(2, "0");
const dd = String(result.getDate()).padStart(2, "0");
return `${result.getFullYear()}-${mm}-${dd}`;
}
const STATUS_LABELS: Record<string, string> = {
issued: "Vystavena",
paid: "Zaplacena",
@@ -491,7 +519,7 @@ export default function InvoiceDetail() {
customer_name: "",
order_id: fromOrderId ? Number(fromOrderId) : null,
issue_date: todayLocalStr(),
due_date: new Date(Date.now() + 14 * 86400000).toISOString().split("T")[0],
due_date: addDaysLocalStr(14),
tax_date: todayLocalStr(),
currency: "CZK",
apply_vat: 1,
@@ -641,15 +669,9 @@ export default function InvoiceDetail() {
customer_id: inv.customer_id || null,
customer_name: inv.customer_name || "",
order_id: inv.order_id || null,
issue_date: inv.issue_date
? new Date(inv.issue_date).toISOString().split("T")[0]
: "",
due_date: inv.due_date
? new Date(inv.due_date).toISOString().split("T")[0]
: "",
tax_date: inv.tax_date
? new Date(inv.tax_date).toISOString().split("T")[0]
: "",
issue_date: normalizeDateStr(inv.issue_date),
due_date: normalizeDateStr(inv.due_date),
tax_date: normalizeDateStr(inv.tax_date),
currency: inv.currency || "CZK",
apply_vat: Number(inv.apply_vat) ? 1 : 0,
vat_rate: Number(inv.vat_rate) || 21,
@@ -713,7 +735,7 @@ export default function InvoiceDetail() {
bankAccountsQuery.isLoading,
bankAccountsQuery.data,
customersQuery.isLoading,
]); // eslint-disable-line react-hooks/exhaustive-deps
]);
// Create mode: populate form from query data
useEffect(() => {
@@ -791,7 +813,7 @@ export default function InvoiceDetail() {
orderDataQuery.data,
companySettings,
bankAccounts,
]); // eslint-disable-line react-hooks/exhaustive-deps
]);
// Capture initial snapshot for dirty-checking once data sync completes.
// Edit mode: captured inside the sync effect from raw query data.
@@ -817,9 +839,7 @@ export default function InvoiceDetail() {
const computedDueDate = useMemo(() => {
if (!form.issue_date) return "";
const d = new Date(form.issue_date);
d.setDate(d.getDate() + dueDays);
return d.toISOString().split("T")[0];
return addDaysLocalStr(dueDays, form.issue_date);
}, [form.issue_date, dueDays]);
// ─── Create mode: customer filtering ───