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