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:
@@ -4,21 +4,14 @@ import { getBusinessDaysInMonth, isHoliday } from "../utils/czech-holidays";
|
||||
import { localDateStr } from "../utils/date";
|
||||
import { getSystemSettings } from "./system-settings";
|
||||
|
||||
/** Get active users whose role has attendance.record permission (or admin role) */
|
||||
/** Get active users whose role has attendance.record permission */
|
||||
async function getAttendanceUsers() {
|
||||
return prisma.users.findMany({
|
||||
where: {
|
||||
is_active: true,
|
||||
roles: {
|
||||
is: {
|
||||
OR: [
|
||||
{ name: "admin" },
|
||||
{
|
||||
role_permissions: {
|
||||
some: { permissions: { name: "attendance.record" } },
|
||||
},
|
||||
},
|
||||
],
|
||||
role_permissions: {
|
||||
some: { permissions: { name: "attendance.record" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -73,11 +66,13 @@ function calcWorkedHours(
|
||||
}
|
||||
|
||||
const roundUp = (d: Date, minutes: number) => {
|
||||
if (!minutes || minutes <= 0) return d;
|
||||
const ms = minutes * 60 * 1000;
|
||||
return new Date(Math.ceil(d.getTime() / ms) * ms);
|
||||
};
|
||||
|
||||
const roundDown = (d: Date, minutes: number) => {
|
||||
if (!minutes || minutes <= 0) return d;
|
||||
const ms = minutes * 60 * 1000;
|
||||
return new Date(Math.floor(d.getTime() / ms) * ms);
|
||||
};
|
||||
@@ -1143,48 +1138,50 @@ export async function bulkCreateAttendance(data: BulkAttendanceData) {
|
||||
let inserted = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const userId of data.user_ids.map(Number)) {
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const date = new Date(yr, mo - 1, day);
|
||||
const dateStr = localDateStr(date);
|
||||
const dow = date.getDay();
|
||||
await prisma.$transaction(async (tx) => {
|
||||
for (const userId of data.user_ids.map(Number)) {
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const date = new Date(yr, mo - 1, day);
|
||||
const dateStr = localDateStr(date);
|
||||
const dow = date.getDay();
|
||||
|
||||
if (dow === 0 || dow === 6) continue;
|
||||
if (dow === 0 || dow === 6) continue;
|
||||
|
||||
if (existingSet.has(`${userId}:${dateStr}`)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
if (existingSet.has(`${userId}:${dateStr}`)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const shiftDate = new Date(yr, mo - 1, day, 12, 0, 0);
|
||||
const shiftDate = new Date(yr, mo - 1, day, 12, 0, 0);
|
||||
|
||||
if (isHoliday(dateStr)) {
|
||||
await prisma.attendance.create({
|
||||
if (isHoliday(dateStr)) {
|
||||
await tx.attendance.create({
|
||||
data: {
|
||||
user_id: userId,
|
||||
shift_date: shiftDate,
|
||||
leave_type: "holiday",
|
||||
leave_hours: 8,
|
||||
},
|
||||
});
|
||||
inserted++;
|
||||
continue;
|
||||
}
|
||||
|
||||
await tx.attendance.create({
|
||||
data: {
|
||||
user_id: userId,
|
||||
shift_date: shiftDate,
|
||||
leave_type: "holiday",
|
||||
leave_hours: 8,
|
||||
arrival_time: new Date(`${dateStr}T${data.arrival_time}:00`),
|
||||
departure_time: new Date(`${dateStr}T${data.departure_time}:00`),
|
||||
break_start: new Date(`${dateStr}T${data.break_start_time}:00`),
|
||||
break_end: new Date(`${dateStr}T${data.break_end_time}:00`),
|
||||
leave_type: "work",
|
||||
},
|
||||
});
|
||||
inserted++;
|
||||
continue;
|
||||
}
|
||||
|
||||
await prisma.attendance.create({
|
||||
data: {
|
||||
user_id: userId,
|
||||
shift_date: shiftDate,
|
||||
arrival_time: new Date(`${dateStr}T${data.arrival_time}:00`),
|
||||
departure_time: new Date(`${dateStr}T${data.departure_time}:00`),
|
||||
break_start: new Date(`${dateStr}T${data.break_start_time}:00`),
|
||||
break_end: new Date(`${dateStr}T${data.break_end_time}:00`),
|
||||
leave_type: "work",
|
||||
},
|
||||
});
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let msg = `Vytvořeno ${inserted} záznamů`;
|
||||
if (skipped > 0) msg += ` (${skipped} přeskočeno — již existují)`;
|
||||
@@ -1212,70 +1209,83 @@ export async function createLeave(data: LeaveData, authUserId: number) {
|
||||
const end = new Date(dateTo);
|
||||
let created = 0;
|
||||
|
||||
const current = new Date(start);
|
||||
while (current <= end) {
|
||||
const dow = current.getDay();
|
||||
if (dow !== 0 && dow !== 6) {
|
||||
const dateStr = localDateStr(current);
|
||||
const shiftDate = new Date(
|
||||
current.getFullYear(),
|
||||
current.getMonth(),
|
||||
current.getDate(),
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
const duplicate = await prisma.attendance.findFirst({
|
||||
where: { user_id: userId, shift_date: shiftDate },
|
||||
});
|
||||
if (duplicate) {
|
||||
return { error: "Pro zvolené datumy již existují záznamy docházky" };
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const current = new Date(start);
|
||||
while (current <= end) {
|
||||
const dow = current.getDay();
|
||||
if (dow !== 0 && dow !== 6 && !isHoliday(localDateStr(current))) {
|
||||
const dateStr = localDateStr(current);
|
||||
const shiftDate = new Date(
|
||||
current.getFullYear(),
|
||||
current.getMonth(),
|
||||
current.getDate(),
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
const duplicate = await tx.attendance.findFirst({
|
||||
where: { user_id: userId, shift_date: shiftDate },
|
||||
});
|
||||
if (duplicate) {
|
||||
throw new Error("Pro zvolené datumy již existují záznamy docházky");
|
||||
}
|
||||
await tx.attendance.create({
|
||||
data: {
|
||||
user_id: userId,
|
||||
shift_date: shiftDate,
|
||||
leave_type: leaveType,
|
||||
leave_hours: data.leave_hours ? Number(data.leave_hours) : 8,
|
||||
notes: data.notes ? String(data.notes) : null,
|
||||
},
|
||||
});
|
||||
created++;
|
||||
}
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
await prisma.attendance.create({
|
||||
data: {
|
||||
user_id: userId,
|
||||
shift_date: shiftDate,
|
||||
leave_type: leaveType,
|
||||
leave_hours: data.leave_hours ? Number(data.leave_hours) : 8,
|
||||
notes: data.notes ? String(data.notes) : null,
|
||||
},
|
||||
});
|
||||
created++;
|
||||
}
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
|
||||
const totalLeaveHours =
|
||||
created * (data.leave_hours ? Number(data.leave_hours) : 8);
|
||||
if (
|
||||
(leaveType === "vacation" || leaveType === "sick") &&
|
||||
totalLeaveHours > 0
|
||||
) {
|
||||
const year = new Date(dateFrom).getFullYear();
|
||||
const existingBalance = await prisma.leave_balances.findFirst({
|
||||
where: { user_id: userId, year },
|
||||
const totalLeaveHours =
|
||||
created * (data.leave_hours ? Number(data.leave_hours) : 8);
|
||||
if (
|
||||
(leaveType === "vacation" || leaveType === "sick") &&
|
||||
totalLeaveHours > 0
|
||||
) {
|
||||
const year = new Date(dateFrom).getFullYear();
|
||||
const existingBalance = await tx.leave_balances.findFirst({
|
||||
where: { user_id: userId, year },
|
||||
});
|
||||
if (existingBalance) {
|
||||
const updateField =
|
||||
leaveType === "vacation" ? "vacation_used" : "sick_used";
|
||||
await tx.leave_balances.update({
|
||||
where: { id: existingBalance.id },
|
||||
data: {
|
||||
[updateField]:
|
||||
Number(existingBalance[updateField]) + totalLeaveHours,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await tx.leave_balances.create({
|
||||
data: {
|
||||
user_id: userId,
|
||||
year,
|
||||
vacation_total: 160,
|
||||
vacation_used: leaveType === "vacation" ? totalLeaveHours : 0,
|
||||
sick_used: leaveType === "sick" ? totalLeaveHours : 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if (existingBalance) {
|
||||
const updateField =
|
||||
leaveType === "vacation" ? "vacation_used" : "sick_used";
|
||||
await prisma.leave_balances.update({
|
||||
where: { id: existingBalance.id },
|
||||
data: {
|
||||
[updateField]: Number(existingBalance[updateField]) + totalLeaveHours,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.leave_balances.create({
|
||||
data: {
|
||||
user_id: userId,
|
||||
year,
|
||||
vacation_total: 160,
|
||||
vacation_used: leaveType === "vacation" ? totalLeaveHours : 0,
|
||||
sick_used: leaveType === "sick" ? totalLeaveHours : 0,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
err.message === "Pro zvolené datumy již existují záznamy docházky"
|
||||
) {
|
||||
return { error: err.message };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return { created, message: `Vytvořeno ${created} záznamů nepřítomnosti` };
|
||||
@@ -1418,8 +1428,17 @@ export async function punchAction(userId: number, data: PunchData) {
|
||||
return { error: "Nemáte aktivní směnu bez přestávky." };
|
||||
}
|
||||
|
||||
const msRound = settings.clock_rounding_minutes * 60 * 1000;
|
||||
const breakStart = new Date(Math.round(now.getTime() / msRound) * msRound);
|
||||
let msRound = settings.clock_rounding_minutes * 60 * 1000;
|
||||
if (
|
||||
!settings.clock_rounding_minutes ||
|
||||
settings.clock_rounding_minutes <= 0
|
||||
) {
|
||||
msRound = 0;
|
||||
}
|
||||
const breakStart =
|
||||
msRound > 0
|
||||
? new Date(Math.round(now.getTime() / msRound) * msRound)
|
||||
: now;
|
||||
const breakEnd = new Date(
|
||||
breakStart.getTime() + settings.break_duration_long * 60 * 1000,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user