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:
@@ -55,7 +55,10 @@ export const AttendanceLeaveSchema = z.object({
|
||||
.optional(),
|
||||
date_from: z.string().min(1, "Datum je povinné"),
|
||||
date_to: z.string().optional(),
|
||||
leave_type: z.string().optional().default("vacation"),
|
||||
leave_type: z
|
||||
.enum(["vacation", "sick", "unpaid", "holiday"])
|
||||
.optional()
|
||||
.default("vacation"),
|
||||
leave_hours: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v))
|
||||
@@ -65,16 +68,8 @@ export const AttendanceLeaveSchema = z.object({
|
||||
|
||||
const ProjectLogSchema = z.object({
|
||||
project_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
|
||||
hours: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v) || 0)
|
||||
.optional()
|
||||
.default(0),
|
||||
minutes: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v) || 0)
|
||||
.optional()
|
||||
.default(0),
|
||||
hours: z.coerce.number().min(0).default(0),
|
||||
minutes: z.coerce.number().min(0).default(0),
|
||||
});
|
||||
|
||||
export const AttendancePunchSchema = z.object({
|
||||
@@ -124,7 +119,10 @@ export const CreateAttendanceSchema = z.object({
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v))
|
||||
.nullish(),
|
||||
leave_type: z.string().optional().default("work"),
|
||||
leave_type: z
|
||||
.enum(["work", "vacation", "sick", "holiday", "unpaid"])
|
||||
.optional()
|
||||
.default("work"),
|
||||
leave_hours: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v))
|
||||
@@ -138,8 +136,13 @@ export const UpdateAttendanceSchema = z.object({
|
||||
break_start: z.union([z.string(), z.null()]).optional(),
|
||||
break_end: z.union([z.string(), z.null()]).optional(),
|
||||
notes: z.string().nullish(),
|
||||
project_id: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
leave_type: z.string().optional(),
|
||||
project_id: z
|
||||
.union([z.number(), z.string(), z.null()])
|
||||
.transform((v) => (v === null ? null : Number(v)))
|
||||
.optional(),
|
||||
leave_type: z
|
||||
.enum(["work", "vacation", "sick", "holiday", "unpaid"])
|
||||
.optional(),
|
||||
leave_hours: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
project_logs: z.array(ProjectLogSchema).optional(),
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export const LoginSchema = z.object({
|
||||
export const TotpVerifySchema = z.object({
|
||||
login_token: z.string().min(1, "Token je povinný"),
|
||||
totp_code: z.string().length(6, "Kód musí mít 6 číslic"),
|
||||
remember_me: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export const TotpBackupSchema = z.object({
|
||||
@@ -16,6 +17,22 @@ export const TotpBackupSchema = z.object({
|
||||
backup_code: z.string().min(1, "Záložní kód je povinný"),
|
||||
});
|
||||
|
||||
export const TotpEnableSchema = z.object({
|
||||
secret: z.string().min(1),
|
||||
code: z.string().min(1),
|
||||
password: z.string().optional(),
|
||||
current_code: z.string().optional(),
|
||||
});
|
||||
|
||||
export const TotpDisableSchema = z.object({
|
||||
code: z.string().min(1),
|
||||
password: z.string().optional(),
|
||||
});
|
||||
|
||||
export const TotpRequiredSchema = z.object({
|
||||
required: z.boolean(),
|
||||
});
|
||||
|
||||
export type LoginInput = z.infer<typeof LoginSchema>;
|
||||
export type TotpVerifyInput = z.infer<typeof TotpVerifySchema>;
|
||||
export type TotpBackupInput = z.infer<typeof TotpBackupSchema>;
|
||||
|
||||
@@ -4,13 +4,21 @@ const InvoiceItemSchema = z.object({
|
||||
description: z.string().nullish(),
|
||||
quantity: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v) || 1)
|
||||
.transform((v) => {
|
||||
const n = Number(v);
|
||||
if (isNaN(n) || n <= 0) throw new Error("Invalid quantity");
|
||||
return n;
|
||||
})
|
||||
.optional()
|
||||
.default(1),
|
||||
unit: z.string().nullish(),
|
||||
unit_price: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v) || 0)
|
||||
.transform((v) => {
|
||||
const n = Number(v);
|
||||
if (isNaN(n)) throw new Error("Invalid unit_price");
|
||||
return n;
|
||||
})
|
||||
.optional()
|
||||
.default(0),
|
||||
vat_rate: z
|
||||
@@ -73,7 +81,10 @@ export const UpdateInvoiceSchema = z.object({
|
||||
bank_iban: z.string().nullish(),
|
||||
bank_account: z.string().nullish(),
|
||||
issued_by: z.string().nullish(),
|
||||
customer_id: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
customer_id: z
|
||||
.union([z.number(), z.string(), z.null()])
|
||||
.transform((v) => (v === null ? null : Number(v)))
|
||||
.optional(),
|
||||
vat_rate: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v))
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateLeaveRequestSchema = z.object({
|
||||
leave_type: z.string().min(1, "Typ nepřítomnosti je povinný"),
|
||||
leave_type: z
|
||||
.enum(["vacation", "sick", "unpaid", "holiday"])
|
||||
.default("vacation"),
|
||||
date_from: z.string().min(1, "Datum od je povinné"),
|
||||
date_to: z.string().min(1, "Datum do je povinné"),
|
||||
notes: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const ReviewLeaveRequestSchema = z.object({
|
||||
status: z.string().min(1, "Stav je povinný"),
|
||||
status: z
|
||||
.enum(["pending", "approved", "rejected", "cancelled"])
|
||||
.default("pending"),
|
||||
reviewer_note: z.string().nullish(),
|
||||
});
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export const CreateQuotationSchema = z.object({
|
||||
.optional()
|
||||
.default(1.0),
|
||||
status: z
|
||||
.enum(["nova", "odeslana", "prijata", "odmitnuta", "dokoncena"])
|
||||
.enum(["nova", "odeslana", "prijata", "odmitnuta", "dokoncena", "active"])
|
||||
.optional()
|
||||
.default("nova"),
|
||||
scope_title: z.string().nullish(),
|
||||
@@ -99,7 +99,7 @@ export const UpdateQuotationSchema = z.object({
|
||||
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
|
||||
.optional(),
|
||||
status: z
|
||||
.enum(["nova", "odeslana", "prijata", "odmitnuta", "dokoncena"])
|
||||
.enum(["nova", "odeslana", "prijata", "odmitnuta", "dokoncena", "active"])
|
||||
.optional(),
|
||||
scope_title: z.string().nullish(),
|
||||
scope_description: z.string().nullish(),
|
||||
|
||||
@@ -96,7 +96,10 @@ export const UpdateOrderSchema = z.object({
|
||||
scope_title: z.string().nullish(),
|
||||
scope_description: z.string().nullish(),
|
||||
notes: z.string().nullish(),
|
||||
customer_id: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
customer_id: z
|
||||
.union([z.number(), z.string(), z.null()])
|
||||
.transform((v) => (v === null ? null : Number(v)))
|
||||
.optional(),
|
||||
vat_rate: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v))
|
||||
|
||||
@@ -34,10 +34,22 @@ export const UpdateProjectSchema = z.object({
|
||||
name: z.string().nullish(),
|
||||
status: z.string().optional(),
|
||||
notes: z.string().nullish(),
|
||||
customer_id: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
responsible_user_id: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
quotation_id: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
order_id: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
customer_id: z
|
||||
.union([z.number(), z.string(), z.null()])
|
||||
.transform((v) => (v === null ? null : Number(v)))
|
||||
.optional(),
|
||||
responsible_user_id: z
|
||||
.union([z.number(), z.string(), z.null()])
|
||||
.transform((v) => (v === null ? null : Number(v)))
|
||||
.optional(),
|
||||
quotation_id: z
|
||||
.union([z.number(), z.string(), z.null()])
|
||||
.transform((v) => (v === null ? null : Number(v)))
|
||||
.optional(),
|
||||
order_id: z
|
||||
.union([z.number(), z.string(), z.null()])
|
||||
.transform((v) => (v === null ? null : Number(v)))
|
||||
.optional(),
|
||||
start_date: z.union([z.string(), z.null()]).optional(),
|
||||
end_date: z.union([z.string(), z.null()]).optional(),
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export const CreateReceivedInvoiceSchema = z.object({
|
||||
.default(0),
|
||||
issue_date: z.string().nullish(),
|
||||
due_date: z.string().nullish(),
|
||||
status: z.string().optional().default("unpaid"),
|
||||
status: z.enum(["unpaid", "paid"]).optional().default("unpaid"),
|
||||
notes: z.string().nullish(),
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ export const UpdateReceivedInvoiceSchema = z.object({
|
||||
issue_date: z.union([z.string(), z.null()]).optional(),
|
||||
due_date: z.union([z.string(), z.null()]).optional(),
|
||||
paid_date: z.union([z.string(), z.null()]).optional(),
|
||||
status: z.string().optional(),
|
||||
status: z.enum(["unpaid", "paid"]).optional(),
|
||||
notes: z.string().nullish(),
|
||||
month: z
|
||||
.union([z.number(), z.string()])
|
||||
|
||||
@@ -2,6 +2,8 @@ import { z } from "zod";
|
||||
|
||||
export const CreateTripSchema = z.object({
|
||||
vehicle_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
|
||||
// user_id is optional here because the route injects it from authData.userId
|
||||
// when the client doesn't provide it (see POST /trips handler)
|
||||
user_id: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v))
|
||||
|
||||
@@ -6,7 +6,10 @@ export const CreateUserSchema = z.object({
|
||||
password: z.string().min(8, "Heslo musí mít alespoň 8 znaků"),
|
||||
first_name: z.string().min(1, "Jméno je povinné"),
|
||||
last_name: z.string().min(1, "Příjmení je povinné"),
|
||||
role_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
|
||||
role_id: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v))
|
||||
.optional(),
|
||||
is_active: z
|
||||
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
|
||||
.optional()
|
||||
@@ -22,7 +25,10 @@ export const UpdateUserSchema = z.object({
|
||||
),
|
||||
first_name: z.string().optional(),
|
||||
last_name: z.string().optional(),
|
||||
role_id: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
role_id: z
|
||||
.union([z.number(), z.string(), z.null()])
|
||||
.transform((v) => (v === null ? null : Number(v)))
|
||||
.optional(),
|
||||
is_active: z
|
||||
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
|
||||
.optional(),
|
||||
|
||||
Reference in New Issue
Block a user