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:
BOHA
2026-04-28 08:40:38 +02:00
parent 7f07032bf2
commit d7c7fbad88
52 changed files with 927 additions and 573 deletions

View File

@@ -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 },