security: fix all Critical and High findings from FLAWS_REPORT audit

- Auth: pessimistic locking on login tokens and refresh token rotation,
  backup code attempt counter, rate limiting verification
- Schema: unique constraints on business numbers, FK relations,
  unsigned/signed alignment, attendance duplicate prevention
- Invoices/PDFs: DOMPurify sanitization, bounded queries in stats
  and alerts, VAT rounding, Puppeteer error handling
- Orders/Offers: transactional parent+child creation, Zod NaN
  refinement, status enums, uniqueness checks
- Projects/Files: path traversal protection, streamed uploads,
  permission guards, query param validation
- Attendance/HR: duplicate checks, ownership validation, GPS
  restrictions, trip distance validation
- Frontend: modal lock reference counting, XSS escaping in print
  HTML, ref mutation fixes, accessibility attributes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-04-24 00:58:35 +02:00
parent 122eee175e
commit 528e55991b
57 changed files with 2355 additions and 1010 deletions

View File

@@ -55,10 +55,9 @@ function computeInvoiceTotals(
const vatAmount = applyVat
? items.reduce((s, i) => {
const base = (Number(i.quantity) || 0) * (Number(i.unit_price) || 0);
return (
s +
base * ((Number(i.vat_rate) || Number(defaultVatRate) || 21) / 100)
);
const vat =
base * ((Number(i.vat_rate) || Number(defaultVatRate) || 21) / 100);
return s + Math.round(vat * 100) / 100;
}, 0)
: 0;
return {
@@ -156,6 +155,31 @@ export {
previewInvoiceNumber as getNextInvoiceNumberPreview,
} from "./numbering.service";
function invoiceTotalWithVat(inv: {
apply_vat: boolean | null;
vat_rate: { toNumber(): number } | null;
currency: string | null;
invoice_items: Array<{
quantity: { toNumber(): number } | null;
unit_price: { toNumber(): number } | null;
vat_rate: { toNumber(): number } | null;
}>;
}) {
const sub = inv.invoice_items.reduce(
(s, i) => s + (Number(i.quantity) || 0) * (Number(i.unit_price) || 0),
0,
);
const vat = inv.apply_vat
? inv.invoice_items.reduce((s, i) => {
const base = (Number(i.quantity) || 0) * (Number(i.unit_price) || 0);
const lineVat =
base * ((Number(i.vat_rate) || Number(inv.vat_rate) || 21) / 100);
return s + Math.round(lineVat * 100) / 100;
}, 0)
: 0;
return sub + vat;
}
export async function getInvoiceStats(queryMonth?: number, queryYear?: number) {
const now = new Date();
const year = queryYear || now.getFullYear();
@@ -163,29 +187,35 @@ export async function getInvoiceStats(queryMonth?: number, queryYear?: number) {
const monthStart = new Date(year, month - 1, 1);
const monthEnd = new Date(year, month, 0, 23, 59, 59);
const startOfYear = new Date(year, 0, 1);
const endOfYear = new Date(year, 11, 31, 23, 59, 59);
const allInvoices = await prisma.invoices.findMany({
include: { invoice_items: true },
});
const [monthInvoices, awaitingInvoices, overdueInvoices] = await Promise.all([
prisma.invoices.findMany({
where: {
issue_date: { gte: monthStart, lte: monthEnd },
},
include: { invoice_items: true },
}),
prisma.invoices.findMany({
where: {
status: "issued",
issue_date: { gte: startOfYear, lte: endOfYear },
},
include: { invoice_items: true },
}),
prisma.invoices.findMany({
where: {
status: "overdue",
issue_date: { gte: startOfYear, lte: endOfYear },
},
include: { invoice_items: true },
}),
]);
const invoiceTotalWithVat = (inv: (typeof allInvoices)[0]) => {
const sub = inv.invoice_items.reduce(
(s, i) => s + (Number(i.quantity) || 0) * (Number(i.unit_price) || 0),
0,
);
const vat = inv.apply_vat
? inv.invoice_items.reduce((s, i) => {
const base = (Number(i.quantity) || 0) * (Number(i.unit_price) || 0);
return (
s +
base * ((Number(i.vat_rate) || Number(inv.vat_rate) || 21) / 100)
);
}, 0)
: 0;
return sub + vat;
};
const aggregateByCurrency = (invoices: typeof allInvoices) => {
const aggregateByCurrency = (
invoices: Parameters<typeof invoiceTotalWithVat>[0][],
) => {
const map: Record<string, number> = {};
for (const inv of invoices) {
const cur = inv.currency || "CZK";
@@ -199,7 +229,9 @@ export async function getInvoiceStats(queryMonth?: number, queryYear?: number) {
}));
};
const sumCzk = async (invoices: typeof allInvoices) => {
const sumCzk = async (
invoices: Parameters<typeof invoiceTotalWithVat>[0][],
) => {
let total = 0;
for (const inv of invoices) {
const amount = invoiceTotalWithVat(inv);
@@ -208,14 +240,7 @@ export async function getInvoiceStats(queryMonth?: number, queryYear?: number) {
return Math.round(total * 100) / 100;
};
const monthInvoices = allInvoices.filter((inv) => {
const issueDate = inv.issue_date ? new Date(inv.issue_date) : null;
return issueDate && issueDate >= monthStart && issueDate <= monthEnd;
});
const paidInvoices = monthInvoices.filter((i) => i.status === "paid");
const awaitingInvoices = allInvoices.filter((i) => i.status === "issued");
const overdueInvoices = allInvoices.filter((i) => i.status === "overdue");
const vatMap: Record<string, number> = {};
for (const inv of monthInvoices) {
@@ -454,8 +479,8 @@ export async function deleteInvoice(id: number) {
await prisma.invoices.delete({ where: { id } });
const year = existing.created_at
? new Date(existing.created_at).getFullYear()
const year = existing.invoice_number
? Number(existing.invoice_number.split("/")[1]) || new Date().getFullYear()
: new Date().getFullYear();
await releaseInvoiceNumber(year);