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

@@ -1,6 +1,7 @@
import { useState } from "react";
import { Link as RouterLink } from "react-router-dom";
import { motion } from "framer-motion";
import { motion, useReducedMotion } from "framer-motion";
import { useQueryClient } from "@tanstack/react-query";
import Box from "@mui/material/Box";
import { useAuth } from "../../context/AuthContext";
import { useAlert } from "../../context/AlertContext";
@@ -16,6 +17,14 @@ interface Vehicle {
name: string;
}
// Standard { success, data, error, message } envelope returned by the API.
interface ApiResult<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
interface TripForm {
vehicle_id: string;
trip_date: string;
@@ -61,6 +70,8 @@ export default function DashQuickActions({
}: DashQuickActionsProps) {
const { hasPermission } = useAuth();
const alert = useAlert();
const queryClient = useQueryClient();
const reduce = useReducedMotion();
const [showTripModal, setShowTripModal] = useState(false);
const [tripSubmitting, setTripSubmitting] = useState(false);
@@ -93,16 +104,17 @@ export default function DashQuickActions({
try {
const response = await apiFetch(`${API_BASE}/vehicles`);
const result = await response.json();
const result: ApiResult<Vehicle[] | { vehicles?: Vehicle[] }> =
await response.json();
if (result.success) {
setTripVehicles(
Array.isArray(result.data)
? result.data
: result.data?.vehicles || [],
: (result.data?.vehicles ?? []),
);
}
} catch {
// vozidla se nenacetla
} catch (e) {
console.error("DashQuickActions: nepodařilo se načíst vozidla", e);
}
};
@@ -113,12 +125,16 @@ export default function DashQuickActions({
}
try {
const response = await apiFetch(`${API_BASE}/trips/last-km/${vehicleId}`);
const result = await response.json();
if (result.success) {
setTripForm((prev) => ({ ...prev, start_km: result.data.last_km }));
const result: ApiResult<{ last_km: number | string }> =
await response.json();
if (result.success && result.data) {
setTripForm((prev) => ({
...prev,
start_km: String(result.data!.last_km ?? ""),
}));
}
} catch {
// last_km se nenacetlo
} catch (e) {
console.error("DashQuickActions: nepodařilo se načíst poslední km", e);
}
};
@@ -139,7 +155,7 @@ export default function DashQuickActions({
if (
tripForm.start_km &&
tripForm.end_km &&
parseInt(tripForm.end_km) <= parseInt(tripForm.start_km)
parseInt(tripForm.end_km, 10) <= parseInt(tripForm.start_km, 10)
) {
errs.end_km = "Musí být větší než počáteční";
}
@@ -161,14 +177,19 @@ export default function DashQuickActions({
headers: { "Content-Type": "application/json" },
body: JSON.stringify(tripForm),
});
const result = await response.json();
const result: ApiResult<unknown> = await response.json();
if (result.success) {
// A new trip changes the trip list and the dashboard vehicle widgets —
// invalidate the broad domains (prefix-matching covers sub-queries).
queryClient.invalidateQueries({ queryKey: ["trips"] });
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
setShowTripModal(false);
alert.success(result.message);
alert.success(result.message ?? "Jízda uložena");
} else {
alert.error(result.error);
alert.error(result.error ?? "Uložení jízdy selhalo");
}
} catch {
} catch (e) {
console.error("DashQuickActions: uložení jízdy selhalo", e);
alert.error("Chyba připojení");
} finally {
setTripSubmitting(false);
@@ -176,8 +197,8 @@ export default function DashQuickActions({
};
const tripDistance = (): number => {
const s = parseInt(tripForm.start_km) || 0;
const e = parseInt(tripForm.end_km) || 0;
const s = parseInt(tripForm.start_km, 10) || 0;
const e = parseInt(tripForm.end_km, 10) || 0;
return e > s ? e - s : 0;
};
@@ -294,8 +315,8 @@ export default function DashQuickActions({
<>
<Box
component={motion.div}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
initial={reduce ? false : { opacity: 0, y: 12 }}
animate={reduce ? undefined : { opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.08 }}
sx={{
display: "grid",