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