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