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:
@@ -135,6 +135,7 @@ export default async function companySettingsRoutes(
|
||||
);
|
||||
|
||||
fastify.get("/", { preHandler: requireAuth }, async (_request, reply) => {
|
||||
// Use upsert to avoid race condition between findFirst and create
|
||||
let settings = await prisma.company_settings.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
@@ -176,50 +177,100 @@ export default async function companySettingsRoutes(
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
settings = await prisma.company_settings.create({
|
||||
data: {
|
||||
company_name: "",
|
||||
quotation_prefix: "N",
|
||||
default_currency: "EUR",
|
||||
default_vat_rate: 21.0,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
company_name: true,
|
||||
street: true,
|
||||
city: true,
|
||||
postal_code: true,
|
||||
country: true,
|
||||
company_id: true,
|
||||
vat_id: true,
|
||||
custom_fields: true,
|
||||
quotation_prefix: true,
|
||||
default_currency: true,
|
||||
default_vat_rate: true,
|
||||
uuid: true,
|
||||
modified_at: true,
|
||||
is_deleted: true,
|
||||
sync_version: true,
|
||||
order_type_code: true,
|
||||
invoice_type_code: true,
|
||||
require_2fa: true,
|
||||
break_threshold_hours: true,
|
||||
break_duration_short: true,
|
||||
break_duration_long: true,
|
||||
clock_rounding_minutes: true,
|
||||
invoice_alert_email: true,
|
||||
leave_notify_email: true,
|
||||
max_login_attempts: true,
|
||||
lockout_minutes: true,
|
||||
max_requests_per_minute: true,
|
||||
available_vat_rates: true,
|
||||
available_currencies: true,
|
||||
smtp_from: true,
|
||||
smtp_from_name: true,
|
||||
offer_number_pattern: true,
|
||||
order_number_pattern: true,
|
||||
invoice_number_pattern: true,
|
||||
},
|
||||
// Wrap create in a transaction to handle race condition:
|
||||
// another request may have created settings between findFirst and create
|
||||
settings = await prisma.$transaction(async (tx) => {
|
||||
// Double-check inside transaction
|
||||
const existing = await tx.company_settings.findFirst({
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) {
|
||||
return tx.company_settings.findUnique({
|
||||
where: { id: existing.id },
|
||||
select: {
|
||||
id: true,
|
||||
company_name: true,
|
||||
street: true,
|
||||
city: true,
|
||||
postal_code: true,
|
||||
country: true,
|
||||
company_id: true,
|
||||
vat_id: true,
|
||||
custom_fields: true,
|
||||
quotation_prefix: true,
|
||||
default_currency: true,
|
||||
default_vat_rate: true,
|
||||
uuid: true,
|
||||
modified_at: true,
|
||||
is_deleted: true,
|
||||
sync_version: true,
|
||||
order_type_code: true,
|
||||
invoice_type_code: true,
|
||||
require_2fa: true,
|
||||
break_threshold_hours: true,
|
||||
break_duration_short: true,
|
||||
break_duration_long: true,
|
||||
clock_rounding_minutes: true,
|
||||
invoice_alert_email: true,
|
||||
leave_notify_email: true,
|
||||
max_login_attempts: true,
|
||||
lockout_minutes: true,
|
||||
max_requests_per_minute: true,
|
||||
available_vat_rates: true,
|
||||
available_currencies: true,
|
||||
smtp_from: true,
|
||||
smtp_from_name: true,
|
||||
offer_number_pattern: true,
|
||||
order_number_pattern: true,
|
||||
invoice_number_pattern: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
return tx.company_settings.create({
|
||||
data: {
|
||||
company_name: "",
|
||||
quotation_prefix: "N",
|
||||
default_currency: "EUR",
|
||||
default_vat_rate: 21.0,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
company_name: true,
|
||||
street: true,
|
||||
city: true,
|
||||
postal_code: true,
|
||||
country: true,
|
||||
company_id: true,
|
||||
vat_id: true,
|
||||
custom_fields: true,
|
||||
quotation_prefix: true,
|
||||
default_currency: true,
|
||||
default_vat_rate: true,
|
||||
uuid: true,
|
||||
modified_at: true,
|
||||
is_deleted: true,
|
||||
sync_version: true,
|
||||
order_type_code: true,
|
||||
invoice_type_code: true,
|
||||
require_2fa: true,
|
||||
break_threshold_hours: true,
|
||||
break_duration_short: true,
|
||||
break_duration_long: true,
|
||||
clock_rounding_minutes: true,
|
||||
invoice_alert_email: true,
|
||||
leave_notify_email: true,
|
||||
max_login_attempts: true,
|
||||
lockout_minutes: true,
|
||||
max_requests_per_minute: true,
|
||||
available_vat_rates: true,
|
||||
available_currencies: true,
|
||||
smtp_from: true,
|
||||
smtp_from_name: true,
|
||||
offer_number_pattern: true,
|
||||
order_number_pattern: true,
|
||||
invoice_number_pattern: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
if (!settings) return error(reply, "Nastavení nenalezeno", 500);
|
||||
@@ -429,7 +480,7 @@ export default async function companySettingsRoutes(
|
||||
: existingOrder,
|
||||
);
|
||||
}
|
||||
data.sync_version = (existing.sync_version ?? 0) + 1;
|
||||
data.sync_version = { increment: 1 };
|
||||
|
||||
await prisma.company_settings.update({
|
||||
where: { id: existing.id },
|
||||
|
||||
Reference in New Issue
Block a user