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