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

@@ -28,6 +28,35 @@ interface Project {
project_number?: string;
}
/** Fields shared by every plan-record save payload the modal emits. */
interface PlanRecordFields {
project_id: number | null;
category: string;
note: string;
}
/** Body for creating a plan entry (a single day or a multi-day range). */
export interface PlanEntryCreateBody extends PlanRecordFields {
user_id: number;
date_from: string;
date_to: string;
}
/** Body for updating an existing plan entry (the entry id is passed separately). */
export interface PlanEntryUpdateBody extends PlanRecordFields {
date_from: string;
date_to: string;
}
/** Body for creating a single-day override. */
export interface PlanOverrideCreateBody extends PlanRecordFields {
user_id: number;
shift_date: string;
}
/** Body for updating an existing override (the override id is passed separately). */
export type PlanOverrideUpdateBody = PlanRecordFields;
export type PlanCellModalMode =
| { kind: "closed" }
| { kind: "create"; userId: number; date: string }
@@ -74,11 +103,11 @@ interface Props {
projects: Project[];
categories: PlanCategory[];
onClose: () => void;
onSaveEntry: (body: any) => Promise<void>;
onUpdateEntry: (id: number, body: any) => Promise<void>;
onSaveEntry: (body: PlanEntryCreateBody) => Promise<void>;
onUpdateEntry: (id: number, body: PlanEntryUpdateBody) => Promise<void>;
onDeleteEntry: (id: number) => Promise<void>;
onSaveOverride: (body: any) => Promise<void>;
onUpdateOverride: (id: number, body: any) => Promise<void>;
onSaveOverride: (body: PlanOverrideCreateBody) => Promise<void>;
onUpdateOverride: (id: number, body: PlanOverrideUpdateBody) => Promise<void>;
onDeleteOverride: (id: number) => Promise<void>;
onCreateOverrideFromRange: (
entryId: number,
@@ -128,6 +157,71 @@ export default function PlanCellModal(props: Props) {
return <EditForm {...props} />;
}
interface EditFormInitial {
date_from: string;
date_to: string;
is_range: boolean;
project_id: number | null;
category: string;
note: string;
}
/**
* Initial form values for every mode kind. Unhandled kinds get a harmless
* default (EditForm renders null for them — but only AFTER its hooks have run).
* Keeping this a plain function (not an inline IIFE with early returns) lets
* EditForm call its useState hooks unconditionally, satisfying the Rules of
* Hooks.
*/
function computeEditFormInitial(
mode: PlanCellModalMode,
activeCategories: { key: string }[],
): EditFormInitial {
if (mode.kind === "create" || mode.kind === "create-override") {
return {
date_from: mode.date,
date_to: mode.date,
is_range: false,
project_id: null,
// Default to "work" when it's active, otherwise the first active
// category, so a new entry never starts on a retired key.
category: activeCategories.some((c) => c.key === "work")
? "work"
: (activeCategories[0]?.key ?? "work"),
note: "",
};
}
if (mode.kind === "edit-range") {
return {
date_from: mode.range.date_from,
date_to: mode.range.date_to,
is_range: mode.range.date_from !== mode.range.date_to,
project_id: mode.range.project_id,
category: mode.range.category,
note: mode.range.note,
};
}
if (mode.kind === "edit-override") {
return {
date_from: mode.date,
date_to: mode.date,
is_range: false,
project_id: mode.cell.project_id,
category: mode.cell.category,
note: mode.cell.note,
};
}
// Unhandled kinds: harmless defaults (EditForm returns null after its hooks).
return {
date_from: "",
date_to: "",
is_range: false,
project_id: null,
category: "work",
note: "",
};
}
function EditForm(props: Props) {
const {
mode,
@@ -140,13 +234,27 @@ function EditForm(props: Props) {
onDeleteOverride,
projects,
} = props;
// An override is a single-day record — both editing one ("edit-override")
// and creating one ("create-override") lock the date and hide the range UI.
const isOverride =
mode.kind === "edit-override" || mode.kind === "create-override";
const activeCategories = props.categories.filter((c) => c.is_active);
// Computed for ALL mode kinds (the helper has a fallback) so the hooks below
// are unconditional.
const initial = computeEditFormInitial(mode, activeCategories);
const [submitting, setSubmitting] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const [dateFrom, setDateFrom] = useState(initial.date_from);
const [dateTo, setDateTo] = useState(initial.date_to);
const [isRange, setIsRange] = useState(initial.is_range);
const [projectId, setProjectId] = useState<number | null>(initial.project_id);
const [category, setCategory] = useState(initial.category);
const [note, setNote] = useState(initial.note);
// Narrow mode to the kinds EditForm actually handles. The call site in
// PlanCellModal only passes "create" | "create-override" | "edit-range" |
// "edit-override" but the parameter type is the full union, so we narrow
// explicitly here.
// Narrow mode to the kinds EditForm actually handles. The call site only ever
// passes the four editable kinds; we guard AFTER all hooks so the hook order
// is identical on every render (Rules of Hooks).
if (
mode.kind !== "create" &&
mode.kind !== "create-override" &&
@@ -156,55 +264,6 @@ function EditForm(props: Props) {
return null;
}
// An override is a single-day record — both editing one ("edit-override")
// and creating one ("create-override") lock the date and hide the range UI.
const isOverride =
mode.kind === "edit-override" || mode.kind === "create-override";
const activeCategories = props.categories.filter((c) => c.is_active);
const initial = (() => {
if (mode.kind === "create" || mode.kind === "create-override") {
return {
date_from: mode.date,
date_to: mode.date,
is_range: false,
project_id: null as number | null,
// Default to "work" when it's active, otherwise the first active
// category, so a new entry never starts on a retired key.
category: activeCategories.some((c) => c.key === "work")
? "work"
: (activeCategories[0]?.key ?? "work"),
note: "",
};
}
if (mode.kind === "edit-range") {
return {
date_from: mode.range.date_from,
date_to: mode.range.date_to,
is_range: mode.range.date_from !== mode.range.date_to,
project_id: mode.range.project_id,
category: mode.range.category,
note: mode.range.note,
};
}
// mode.kind === "edit-override" (narrowed above)
return {
date_from: mode.date,
date_to: mode.date,
is_range: false,
project_id: mode.cell.project_id,
category: mode.cell.category,
note: mode.cell.note,
};
})();
const [dateFrom, setDateFrom] = useState(initial.date_from);
const [dateTo, setDateTo] = useState(initial.date_to);
const [isRange, setIsRange] = useState(initial.is_range);
const [projectId, setProjectId] = useState<number | null>(initial.project_id);
const [category, setCategory] = useState(initial.category);
const [note, setNote] = useState(initial.note);
const title =
mode.kind === "create"
? "Nový záznam plánu"