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