fix: security, validation, and data integrity fixes across 53 files
- Auth: HS256 algorithm restriction on JWT verify, timing-safe bcrypt
for inactive/locked users, locked_until check in loadAuthData, TOTP
fixes (async bcrypt, BigInt conversion, future-code counter fix)
- Validation: Zod enums for leave_type/status, numeric transforms on
foreign keys, VAT 0% coercion fix (Number(v)||21 → v!=null checks)
- Permissions: requirePermission on attendance PUT, attendance_users
and project_logs access checks, trips users filtered by trips.record
- Prisma queries: fixed roles.is:{OR} pattern (doesn't work on to-one
relations), attendance_users now filters by attendance.record only
- Transactions: wrapped deleteOrder, createOrder, updateUser, deleteUser,
duplicateOffer, bulkCreateAttendance, createLeave, scope-templates,
leave-requests, company-settings, profile updates
- Frontend: mountedRef reset in useListData, blob URL cleanup on unmount,
null checks on date fields, AdminDatePicker min/max for HH:mm
- Security headers: COOP, CORP, CSP frame-ancestors/form-action/base-uri
- Other: exchange-rate cache TTL, invoice-alert midnight comparison fix,
numbering.service releaseSequence no-op, nas-offers filename sanitize,
Content-Disposition header injection fix, mojibake Czech strings
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
} from "../../schemas/received-invoices.schema";
|
||||
import { nasFinancialsManager } from "../../services/nas-financials-manager";
|
||||
import { toCzk } from "../../services/exchange-rates";
|
||||
import path from "path";
|
||||
|
||||
const VALID_STATUSES = ["unpaid", "paid"] as const;
|
||||
|
||||
@@ -126,9 +127,17 @@ export default async function receivedInvoicesRoutes(
|
||||
return Math.round(total * 100) / 100;
|
||||
};
|
||||
|
||||
// Also get all-time unpaid
|
||||
// Also get all-time unpaid — use DB-level aggregation for count/sums
|
||||
const stats = await prisma.received_invoices.aggregate({
|
||||
where: { status: { not: "paid" }, is_deleted: false },
|
||||
_sum: { amount: true, amount_czk: true },
|
||||
_count: true,
|
||||
});
|
||||
|
||||
// We still need per-currency breakdown for unpaid, so fetch only those
|
||||
const allUnpaid = await prisma.received_invoices.findMany({
|
||||
where: { status: { not: "paid" } },
|
||||
where: { status: { not: "paid" }, is_deleted: false },
|
||||
select: { amount: true, currency: true },
|
||||
});
|
||||
|
||||
return success(reply, {
|
||||
@@ -137,8 +146,10 @@ export default async function receivedInvoicesRoutes(
|
||||
vat_month: aggregateByCurrency(monthInvoices, "vat_amount"),
|
||||
vat_month_czk: await sumCzk(monthInvoices, "vat_amount"),
|
||||
unpaid: aggregateByCurrency(allUnpaid, "amount"),
|
||||
unpaid_czk: await sumCzk(allUnpaid, "amount"),
|
||||
unpaid_count: allUnpaid.length,
|
||||
unpaid_czk: stats._sum.amount_czk
|
||||
? Math.round(Number(stats._sum.amount_czk) * 100) / 100
|
||||
: await sumCzk(allUnpaid, "amount"),
|
||||
unpaid_count: stats._count,
|
||||
month_count: monthInvoices.length,
|
||||
});
|
||||
},
|
||||
@@ -188,12 +199,10 @@ export default async function receivedInvoicesRoutes(
|
||||
if (!nasFile) return error(reply, "Soubor na NAS nenalezen", 404);
|
||||
|
||||
const mime = invoice.file_mime || "application/pdf";
|
||||
const safeFileName = invoice.file_name.replace(/[\r\n"]/g, "");
|
||||
return reply
|
||||
.type(mime)
|
||||
.header(
|
||||
"Content-Disposition",
|
||||
`inline; filename="${invoice.file_name}"`,
|
||||
)
|
||||
.header("Content-Disposition", `inline; filename="${safeFileName}"`)
|
||||
.send(nasFile.data);
|
||||
},
|
||||
);
|
||||
@@ -315,7 +324,9 @@ export default async function receivedInvoicesRoutes(
|
||||
status: "unpaid",
|
||||
notes: meta.notes ? String(meta.notes) : null,
|
||||
uploaded_by: request.authData?.userId,
|
||||
file_name: file.name,
|
||||
file_name: nasResult.filePath
|
||||
? path.basename(nasResult.filePath)
|
||||
: file.name,
|
||||
file_mime: file.mime,
|
||||
file_size: file.size,
|
||||
},
|
||||
@@ -364,7 +375,7 @@ export default async function receivedInvoicesRoutes(
|
||||
vat_rate: vatRate,
|
||||
vat_amount:
|
||||
vatRate > 0
|
||||
? Math.round((amount - amount / (1 + vatRate / 100)) * 100) / 100
|
||||
? Math.round(((amount * vatRate) / 100) * 100) / 100
|
||||
: 0,
|
||||
issue_date: body.issue_date
|
||||
? new Date(String(body.issue_date))
|
||||
@@ -544,6 +555,9 @@ export default async function receivedInvoicesRoutes(
|
||||
});
|
||||
if (!existing) return error(reply, "Přijatá faktura nenalezena", 404);
|
||||
|
||||
// Delete DB record first, then NAS file — avoids orphaned file if DB delete fails
|
||||
await prisma.received_invoices.delete({ where: { id } });
|
||||
|
||||
if (existing.file_name) {
|
||||
const relPath = nasFinancialsManager.buildReceivedPath(
|
||||
existing.file_name,
|
||||
@@ -552,8 +566,6 @@ export default async function receivedInvoicesRoutes(
|
||||
);
|
||||
nasFinancialsManager.deleteReceivedInvoice(relPath);
|
||||
}
|
||||
|
||||
await prisma.received_invoices.delete({ where: { id } });
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
|
||||
Reference in New Issue
Block a user