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

@@ -10,19 +10,23 @@ interface CnbRate {
amount: number;
}
const rateCache = new Map<string, Record<string, number>>();
let rateCacheTime = 0;
// Per-key cache: each key carries its OWN fetch timestamp so a hit on one date
// never refreshes the TTL of another. A stale entry is only re-fetched (and
// only that key is checked for the stale fallback).
const rateCache = new Map<
string,
{ rates: Record<string, number>; time: number }
>();
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
const FETCH_TIMEOUT_MS = 8000; // abort a hung ČNB endpoint
const inflight = new Map<string, Promise<Record<string, number>>>();
async function fetchRatesForDate(
date?: string,
): Promise<Record<string, number>> {
const key = date || "today";
if (Date.now() - rateCacheTime > CACHE_TTL_MS) {
rateCache.clear();
}
if (rateCache.has(key)) return rateCache.get(key)!;
const cached = rateCache.get(key);
if (cached && Date.now() - cached.time <= CACHE_TTL_MS) return cached.rates;
if (inflight.has(key)) return inflight.get(key)!;
const promise = (async () => {
@@ -30,22 +34,29 @@ async function fetchRatesForDate(
let url = "https://api.cnb.cz/cnbapi/exrates/daily?lang=EN";
if (date) url += `&date=${date}`;
const response = await fetch(url);
const response = await fetch(url, {
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});
if (!response.ok) throw new Error(`CNB API: ${response.status}`);
const data = (await response.json()) as { rates: CnbRate[] };
const data = (await response.json()) as { rates?: CnbRate[] };
if (!Array.isArray(data.rates)) {
throw new Error("CNB API: neočekávaný formát odpovědi (rates)");
}
const rates: Record<string, number> = { CZK: 1 };
for (const r of data.rates) {
rates[r.currencyCode] = r.rate / r.amount;
}
rateCache.set(key, rates);
rateCacheTime = Date.now();
rateCache.set(key, { rates, time: Date.now() });
return rates;
} catch (err) {
console.error("Failed to fetch CNB exchange rates:", err);
if (rateCache.has("today")) return rateCache.get("today")!;
// Stale-cache fallback for the SAME key (e.g. a hung/aborted refresh of
// an entry that is past TTL — better to serve yesterday's rate than fail).
const stale = rateCache.get(key);
if (stale) return stale.rates;
throw new Error("Nepodařilo se získat aktuální kurzy z ČNB");
} finally {
inflight.delete(key);
@@ -56,6 +67,18 @@ async function fetchRatesForDate(
return promise;
}
/**
* Test-only: clear the module-level rate cache (and any in-flight fetches).
* The cache persists for the process lifetime, so tests that assert on
* specific fetched rates must reset it between runs to avoid order-dependent
* results (a populated "today" entry would otherwise short-circuit the mock).
* Not used by runtime code.
*/
export function __resetRateCacheForTest(): void {
rateCache.clear();
inflight.clear();
}
/** Convert an amount from a given currency to CZK using CNB rates */
export async function toCzk(
amount: number,