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

@@ -7,6 +7,7 @@ import { getSystemSettings } from "./system-settings";
interface AlertInvoice {
id: number;
type: "created" | "received";
alert_type: "due" | "3days";
number: string;
counterparty: string;
amount: string;
@@ -20,7 +21,8 @@ function escapeHtml(str: string): string {
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function formatAmount(n: number | { toNumber?: () => number }): string {
@@ -43,125 +45,134 @@ export async function checkInvoiceAlerts(): Promise<void> {
in3days.setDate(in3days.getDate() + 3);
const in3daysStr = localDateStr(in3days);
const alerts: AlertInvoice[] = [];
// Classify a due date into an alert type/label, or null if it doesn't match.
const classify = (
dueDate: Date,
): { alertType: "due" | "3days"; daysLabel: string } | null => {
const dueDateStr = localDateStr(dueDate);
if (dueDateStr === todayStr)
return { alertType: "due", daysLabel: "splatnost dnes" };
if (dueDateStr === in3daysStr)
return { alertType: "3days", daysLabel: "splatnost za 3 dny" };
return null;
};
// --- Created invoices (customer owes us) ---
const createdInvoices = await prisma.invoices.findMany({
where: {
status: { in: ["issued", "overdue"] },
due_date: { gte: today, lte: in3days },
},
select: {
id: true,
invoice_number: true,
due_date: true,
currency: true,
customers: { select: { name: true } },
invoice_items: { select: { quantity: true, unit_price: true } },
},
});
for (const inv of createdInvoices) {
if (!inv.due_date) continue;
const dueDateStr = localDateStr(new Date(inv.due_date));
let alertType: string | null = null;
let daysLabel = "";
if (dueDateStr === todayStr) {
alertType = "due";
daysLabel = "splatnost dnes";
} else if (dueDateStr === in3daysStr) {
alertType = "3days";
daysLabel = "splatnost za 3 dny";
}
if (!alertType) continue;
const alreadySent = await prisma.invoice_alert_log.findUnique({
// --- Gather candidates (created + received) before touching the alert log ---
const [createdInvoices, receivedInvoices] = await Promise.all([
prisma.invoices.findMany({
where: {
invoice_type_invoice_id_alert_type: {
invoice_type: "created",
invoice_id: inv.id,
alert_type: alertType,
status: { in: ["issued", "overdue"] },
due_date: { gte: today, lte: in3days },
},
select: {
id: true,
invoice_number: true,
due_date: true,
currency: true,
apply_vat: true,
vat_rate: true,
customers: { select: { name: true } },
invoice_items: {
select: { quantity: true, unit_price: true, vat_rate: true },
},
},
});
if (alreadySent) continue;
}),
prisma.received_invoices.findMany({
where: {
status: "unpaid",
due_date: { gte: today, lte: in3days },
},
select: {
id: true,
invoice_number: true,
supplier_name: true,
amount: true,
currency: true,
due_date: true,
},
}),
]);
// Build the candidate set (matched-on-date, not yet de-duped against the log)
// so we can batch-load the alert log in a single query.
type Candidate = AlertInvoice;
const createdCandidates: Candidate[] = [];
for (const inv of createdInvoices) {
if (!inv.due_date) continue;
const c = classify(new Date(inv.due_date));
if (!c) continue;
// Gross total (subtotal + VAT) — consistent with the gross amounts shown
// for received invoices. VAT is computed per line, rounded to 2 decimals.
const subtotal = inv.invoice_items.reduce(
(sum, item) => sum + Number(item.quantity) * Number(item.unit_price),
0,
);
const vatTotal = inv.apply_vat
? inv.invoice_items.reduce((sum, item) => {
const base = Number(item.quantity) * Number(item.unit_price);
const rate = Number(item.vat_rate) || Number(inv.vat_rate) || 21;
return sum + Math.round(base * (rate / 100) * 100) / 100;
}, 0)
: 0;
const gross = Math.round((subtotal + vatTotal) * 100) / 100;
alerts.push({
createdCandidates.push({
id: inv.id,
type: "created",
alert_type: c.alertType,
number: inv.invoice_number || `#${inv.id}`,
counterparty: inv.customers?.name || "—",
amount: formatAmount(subtotal),
amount: formatAmount(gross),
currency: inv.currency || "CZK",
due_date: localDateCzStr(new Date(inv.due_date)),
days_label: daysLabel,
days_label: c.daysLabel,
});
}
// --- Received invoices (we owe supplier) ---
const receivedInvoices = await prisma.received_invoices.findMany({
where: {
status: "unpaid",
due_date: { gte: today, lte: in3days },
},
select: {
id: true,
invoice_number: true,
supplier_name: true,
amount: true,
currency: true,
due_date: true,
},
});
const receivedCandidates: Candidate[] = [];
for (const inv of receivedInvoices) {
if (!inv.due_date) continue;
const dueDateStr = localDateStr(new Date(inv.due_date));
const c = classify(new Date(inv.due_date));
if (!c) continue;
let alertType: string | null = null;
let daysLabel = "";
if (dueDateStr === todayStr) {
alertType = "due";
daysLabel = "splatnost dnes";
} else if (dueDateStr === in3daysStr) {
alertType = "3days";
daysLabel = "splatnost za 3 dny";
}
if (!alertType) continue;
const alreadySent = await prisma.invoice_alert_log.findUnique({
where: {
invoice_type_invoice_id_alert_type: {
invoice_type: "received",
invoice_id: inv.id,
alert_type: alertType,
},
},
});
if (alreadySent) continue;
alerts.push({
receivedCandidates.push({
id: inv.id,
type: "received",
alert_type: c.alertType,
number: inv.invoice_number || inv.supplier_name,
counterparty: inv.supplier_name,
// received_invoices.amount is GROSS (VAT-inclusive) by convention.
amount: formatAmount(inv.amount),
currency: inv.currency,
due_date: localDateCzStr(new Date(inv.due_date)),
days_label: daysLabel,
days_label: c.daysLabel,
});
}
const candidates = [...createdCandidates, ...receivedCandidates];
if (candidates.length === 0) return;
// Batch-load every already-sent log entry for the candidate set in ONE query
// (instead of a findUnique per invoice).
const sentLogs = await prisma.invoice_alert_log.findMany({
where: {
OR: candidates.map((c) => ({
invoice_type: c.type,
invoice_id: c.id,
alert_type: c.alert_type,
})),
},
select: { invoice_type: true, invoice_id: true, alert_type: true },
});
const sentKeys = new Set(
sentLogs.map((l) => `${l.invoice_type}:${l.invoice_id}:${l.alert_type}`),
);
const alerts: AlertInvoice[] = candidates.filter(
(c) => !sentKeys.has(`${c.type}:${c.id}:${c.alert_type}`),
);
if (alerts.length === 0) return;
// --- Build summary email ---
@@ -223,21 +234,21 @@ export async function checkInvoiceAlerts(): Promise<void> {
console.log(`InvoiceAlerts: Sent ${alerts.length} alert(s) to ${alertEmail}`);
// Mark alerts as sent only after successful delivery
for (const a of alerts) {
await prisma.invoice_alert_log.create({
data: {
// Mark alerts as sent only after successful delivery. One INSERT with
// skipDuplicates so a unique-constraint hit (concurrent run) on one row
// doesn't abort logging the rest.
try {
await prisma.invoice_alert_log.createMany({
data: alerts.map((a) => ({
invoice_type: a.type,
invoice_id: a.id,
alert_type:
a.type === "created"
? a.days_label.includes("dnes")
? "due"
: "3days"
: a.days_label.includes("dnes")
? "due"
: "3days",
},
alert_type: a.alert_type,
})),
skipDuplicates: true,
});
} catch (err) {
// Non-fatal: the email already went out. Log so a failed bookkeeping
// write is visible (and may cause a duplicate alert on the next run).
console.error("InvoiceAlerts: failed to record alert log:", err);
}
}