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

@@ -6,12 +6,14 @@ const QuotationItemSchema = z.object({
quantity: z
.union([z.number(), z.string()])
.transform((v) => Number(v) || 1)
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional()
.default(1),
unit: z.string().nullish(),
unit_price: z
.union([z.number(), z.string()])
.transform((v) => Number(v) || 0)
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional()
.default(0),
is_included_in_total: z
@@ -21,6 +23,7 @@ const QuotationItemSchema = z.object({
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional(),
});
@@ -31,6 +34,7 @@ const ScopeSectionSchema = z.object({
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional(),
});
@@ -40,6 +44,7 @@ export const CreateQuotationSchema = z.object({
customer_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.nullish(),
valid_until: z.string().nullish(),
currency: z.string().optional().default("CZK"),
@@ -47,6 +52,7 @@ export const CreateQuotationSchema = z.object({
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional()
.default(21.0),
apply_vat: z
@@ -56,9 +62,13 @@ export const CreateQuotationSchema = z.object({
exchange_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional()
.default(1.0),
status: z.string().optional().default("active"),
status: z
.enum(["nova", "odeslana", "prijata", "odmitnuta", "dokoncena"])
.optional()
.default("nova"),
scope_title: z.string().nullish(),
scope_description: z.string().nullish(),
items: z.array(QuotationItemSchema).optional(),
@@ -70,6 +80,7 @@ export const UpdateQuotationSchema = z.object({
customer_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional(),
valid_until: z.union([z.string(), z.null()]).optional(),
currency: z.string().optional(),
@@ -77,6 +88,7 @@ export const UpdateQuotationSchema = z.object({
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional(),
apply_vat: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
@@ -84,8 +96,11 @@ export const UpdateQuotationSchema = z.object({
exchange_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional(),
status: z
.enum(["nova", "odeslana", "prijata", "odmitnuta", "dokoncena"])
.optional(),
status: z.string().optional(),
scope_title: z.string().nullish(),
scope_description: z.string().nullish(),
items: z.array(QuotationItemSchema).optional(),

View File

@@ -6,12 +6,14 @@ const OrderItemSchema = z.object({
quantity: z
.union([z.number(), z.string()])
.transform((v) => Number(v) || 1)
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional()
.default(1),
unit: z.string().nullish(),
unit_price: z
.union([z.number(), z.string()])
.transform((v) => Number(v) || 0)
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional()
.default(0),
is_included_in_total: z
@@ -21,6 +23,7 @@ const OrderItemSchema = z.object({
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional(),
});
@@ -31,11 +34,15 @@ const OrderSectionSchema = z.object({
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional(),
});
export const CreateOrderFromQuotationSchema = z.object({
quotationId: z.union([z.number(), z.string()]).transform((v) => Number(v)),
quotationId: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" }),
customerOrderNumber: z.string().optional().default(""),
});
@@ -45,17 +52,23 @@ export const CreateOrderSchema = z.object({
quotation_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.nullish(),
customer_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.nullish(),
status: z.string().optional().default("prijata"),
status: z
.enum(["prijata", "v_realizaci", "dokoncena", "zrusena"])
.optional()
.default("prijata"),
currency: z.string().optional().default("CZK"),
language: z.string().optional().default("cs"),
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional()
.default(21.0),
apply_vat: z
@@ -65,6 +78,7 @@ export const CreateOrderSchema = z.object({
exchange_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional()
.default(1.0),
scope_title: z.string().nullish(),
@@ -76,7 +90,7 @@ export const CreateOrderSchema = z.object({
export const UpdateOrderSchema = z.object({
customer_order_number: z.string().nullish(),
status: z.string().optional(),
status: z.enum(["prijata", "v_realizaci", "dokoncena", "zrusena"]).optional(),
currency: z.string().optional(),
language: z.string().optional(),
scope_title: z.string().nullish(),
@@ -86,6 +100,7 @@ export const UpdateOrderSchema = z.object({
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.optional(),
apply_vat: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())