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:
@@ -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 ───
|
||||
|
||||
Reference in New Issue
Block a user