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

@@ -56,54 +56,58 @@ function decodeCustomFields(raw: string | null): {
export default async function customersRoutes(
fastify: FastifyInstance,
): Promise<void> {
fastify.get("/", { preHandler: requireAuth }, async (request, reply) => {
const { page, limit, skip, sort, order, search } = parsePagination(
request.query as Record<string, unknown>,
);
const sortField = ALLOWED_SORT_FIELDS.includes(sort) ? sort : "name";
const where = search
? {
OR: [
{ name: { contains: search } },
{ company_id: { contains: search } },
],
}
: {};
const [customers, total] = await Promise.all([
prisma.customers.findMany({
where,
skip,
take: limit,
orderBy: { [sortField]: order },
include: { _count: { select: { quotations: true } } },
}),
prisma.customers.count({ where }),
]);
const enriched = customers.map((c) => {
const { custom_fields, customer_field_order } = decodeCustomFields(
c.custom_fields,
fastify.get(
"/",
{ preHandler: requirePermission("customers.view") },
async (request, reply) => {
const { page, limit, skip, sort, order, search } = parsePagination(
request.query as Record<string, unknown>,
);
return {
...c,
custom_fields,
customer_field_order,
quotation_count: c._count?.quotations ?? 0,
};
});
const sortField = ALLOWED_SORT_FIELDS.includes(sort) ? sort : "name";
return reply.send({
success: true,
data: enriched,
pagination: buildPaginationMeta(total, page, limit),
});
});
const where = search
? {
OR: [
{ name: { contains: search } },
{ company_id: { contains: search } },
],
}
: {};
const [customers, total] = await Promise.all([
prisma.customers.findMany({
where,
skip,
take: limit,
orderBy: { [sortField]: order },
include: { _count: { select: { quotations: true } } },
}),
prisma.customers.count({ where }),
]);
const enriched = customers.map((c) => {
const { custom_fields, customer_field_order } = decodeCustomFields(
c.custom_fields,
);
return {
...c,
custom_fields,
customer_field_order,
quotation_count: c._count?.quotations ?? 0,
};
});
return reply.send({
success: true,
data: enriched,
pagination: buildPaginationMeta(total, page, limit),
});
},
);
fastify.get<{ Params: { id: string } }>(
"/:id",
{ preHandler: requireAuth },
{ preHandler: requirePermission("customers.view") },
async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;