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

@@ -8,11 +8,14 @@ import {
warehouseStockStatusOptions,
warehouseBelowMinimumOptions,
warehouseCategoryListOptions,
warehouseMovementLogOptions,
type WarehouseItem,
type MovementLogRow,
} from "../lib/queries/warehouse";
import apiFetch from "../utils/api";
import { formatCurrency } from "../utils/formatters";
import { jsonQuery } from "../lib/apiAdapter";
import { formatCurrency, formatDate } from "../utils/formatters";
import {
Alert,
Button,
Card,
DataTable,
@@ -43,16 +46,6 @@ interface ProjectConsumptionRow {
total_value: number;
}
interface MovementLogRow {
date: string;
type: string;
document_number: string;
item_name: string;
quantity: number;
unit_price: number;
supplier_or_project: string;
}
const TABS: TabDef[] = [
{ value: "stock-status", label: "Stav zásob" },
{ value: "project-consumption", label: "Spotřeba projektů" },
@@ -260,34 +253,36 @@ function ProjectConsumptionTab({
name: string;
}[]) ?? [];
const [data, setData] = useState<ProjectConsumptionRow[] | null>(null);
const [loading, setLoading] = useState(false);
const [fetched, setFetched] = useState(false);
// Filters applied on "Zobrazit"; the query key carries them so results land in
// the ["warehouse"] cache and refresh on warehouse mutations.
const [applied, setApplied] = useState<{
projectId: number | "";
dateFrom: string;
dateTo: string;
} | null>(null);
const handleFetch = async () => {
setLoading(true);
setFetched(true);
try {
const {
data,
isFetching: loading,
error,
} = useQuery({
queryKey: ["warehouse", "project-consumption", applied],
enabled: applied !== null,
queryFn: () => {
const params = new URLSearchParams();
if (projectId) params.set("project_id", String(projectId));
if (dateFrom) params.set("date_from", dateFrom);
if (dateTo) params.set("date_to", dateTo);
if (applied?.projectId)
params.set("project_id", String(applied.projectId));
if (applied?.dateFrom) params.set("date_from", applied.dateFrom);
if (applied?.dateTo) params.set("date_to", applied.dateTo);
const qs = params.toString();
const response = await apiFetch(
return jsonQuery<ProjectConsumptionRow[]>(
`/api/admin/warehouse/reports/project-consumption${qs ? `?${qs}` : ""}`,
);
const result = await response.json();
if (result.success) {
setData(result.data ?? []);
} else {
setData([]);
}
} catch {
setData([]);
} finally {
setLoading(false);
}
};
},
});
const fetched = applied !== null;
const handleFetch = () => setApplied({ projectId, dateFrom, dateTo });
const columns: DataColumn<ProjectConsumptionRow & { _idx: number }>[] = [
{
@@ -352,7 +347,15 @@ function ProjectConsumptionTab({
{fetched && loading && <LoadingState />}
{fetched && !loading && data && (
{fetched && !loading && error && (
<Alert severity="error">
{error instanceof Error
? error.message
: "Nepodařilo se načíst spotřebu projektů"}
</Alert>
)}
{fetched && !loading && !error && data && (
<Card>
<DataTable<ProjectConsumptionRow & { _idx: number }>
columns={columns}
@@ -383,34 +386,29 @@ function MovementLogTab({
dateTo: string;
setDateTo: (v: string) => void;
}) {
const [data, setData] = useState<MovementLogRow[] | null>(null);
const [loading, setLoading] = useState(false);
const [fetched, setFetched] = useState(false);
// Filters applied on "Zobrazit"; uses the shared movement-log query option so
// results land in the ["warehouse"] cache and refresh on warehouse mutations.
const [applied, setApplied] = useState<{
type: string;
dateFrom: string;
dateTo: string;
} | null>(null);
const handleFetch = async () => {
setLoading(true);
setFetched(true);
try {
const params = new URLSearchParams();
if (type) params.set("type", type);
if (dateFrom) params.set("date_from", dateFrom);
if (dateTo) params.set("date_to", dateTo);
const qs = params.toString();
const response = await apiFetch(
`/api/admin/warehouse/reports/movement-log${qs ? `?${qs}` : ""}`,
);
const result = await response.json();
if (result.success) {
setData(result.data ?? []);
} else {
setData([]);
}
} catch {
setData([]);
} finally {
setLoading(false);
}
};
const {
data,
isFetching: loading,
error,
} = useQuery({
...warehouseMovementLogOptions({
type: applied?.type || undefined,
date_from: applied?.dateFrom || undefined,
date_to: applied?.dateTo || undefined,
}),
enabled: applied !== null,
});
const fetched = applied !== null;
const handleFetch = () => setApplied({ type, dateFrom, dateTo });
const TYPE_LABEL: Record<string, string> = {
receipt: "Příjem",
@@ -423,7 +421,7 @@ function MovementLogTab({
header: "Datum",
width: "12%",
mono: true,
render: (row) => row.date,
render: (row) => formatDate(row.date),
},
{
key: "type",
@@ -501,7 +499,15 @@ function MovementLogTab({
{fetched && loading && <LoadingState />}
{fetched && !loading && data && (
{fetched && !loading && error && (
<Alert severity="error">
{error instanceof Error
? error.message
: "Nepodařilo se načíst pohyby"}
</Alert>
)}
{fetched && !loading && !error && data && (
<Card>
<DataTable<MovementLogRow & { _idx: number }>
columns={columns}