fix: oprava kritických bezpečnostních chyb a bugů z code review
- SEC-1: nahrazen exec('fsutil') za PHP-native is_link()+realpath() v NasFileManager - eliminace command injection
- SEC-2: přidáno ověření aktuálního hesla při změně hesla (profile.php + DashProfile.jsx)
- BUG-1: attendance punch obalen do transakce s SELECT FOR UPDATE - prevence race condition při dvojkliku
- BUG-2: eliminován N+1 SQL dotaz pro VAT v invoice listu - výpočet přesunut do subquery
- BUG-5/6: delete a update attendance záznamů obaleny do transakcí - prevence nekonzistentního stavu
- BUG-7: opravena duplikace nabídky - přidáno chybějící pole unit v offer items
ESLint: 0 errors | PHPCS: 0 errors | Build: OK
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -261,106 +261,122 @@ function handlePunch(PDO $pdo, int $userId): void
|
||||
$lat = isset($input['latitude']) && $input['latitude'] !== '' ? (float)$input['latitude'] : null;
|
||||
$lng = isset($input['longitude']) && $input['longitude'] !== '' ? (float)$input['longitude'] : null;
|
||||
$accuracy = isset($input['accuracy']) && $input['accuracy'] !== '' ? (float)$input['accuracy'] : null;
|
||||
$address = !empty($input['address']) ? $input['address'] : null;
|
||||
$address = !empty($input['address']) ? mb_substr($input['address'], 0, 500) : null;
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, user_id, shift_date, arrival_time, break_start, break_end,
|
||||
departure_time, notes, project_id, leave_type, created_at
|
||||
FROM attendance
|
||||
WHERE user_id = ? AND departure_time IS NULL AND (leave_type IS NULL OR leave_type = 'work')
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
");
|
||||
$stmt->execute([$userId]);
|
||||
$ongoingShift = $stmt->fetch();
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, user_id, shift_date, arrival_time, break_start, break_end,
|
||||
departure_time, notes, project_id, leave_type, created_at
|
||||
FROM attendance
|
||||
WHERE user_id = ? AND departure_time IS NULL AND (leave_type IS NULL OR leave_type = 'work')
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
FOR UPDATE
|
||||
");
|
||||
$stmt->execute([$userId]);
|
||||
$ongoingShift = $stmt->fetch();
|
||||
|
||||
if ($action === 'arrival' && !$ongoingShift) {
|
||||
$now = roundUpTo15Minutes($rawNow);
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO attendance
|
||||
(user_id, shift_date, arrival_time, arrival_lat, arrival_lng, arrival_accuracy, arrival_address)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
');
|
||||
$stmt->execute([$userId, $today, $now, $lat, $lng, $accuracy, $address]);
|
||||
if ($action === 'arrival' && !$ongoingShift) {
|
||||
$now = roundUpTo15Minutes($rawNow);
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO attendance
|
||||
(user_id, shift_date, arrival_time, arrival_lat, arrival_lng, arrival_accuracy, arrival_address)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
');
|
||||
$stmt->execute([$userId, $today, $now, $lat, $lng, $accuracy, $address]);
|
||||
|
||||
AuditLog::logCreate('attendance', (int)$pdo->lastInsertId(), [
|
||||
'arrival_time' => $now,
|
||||
'location' => $address,
|
||||
], 'Příchod zaznamenán');
|
||||
AuditLog::logCreate('attendance', (int)$pdo->lastInsertId(), [
|
||||
'arrival_time' => $now,
|
||||
'location' => $address,
|
||||
], 'Příchod zaznamenán');
|
||||
|
||||
successResponse(null, 'Příchod zaznamenán');
|
||||
} elseif ($ongoingShift) {
|
||||
switch ($action) {
|
||||
case 'break_start':
|
||||
if ($ongoingShift['arrival_time'] && !$ongoingShift['break_start']) {
|
||||
$breakStart = roundToNearest10Minutes($rawNow);
|
||||
$breakEnd = date('Y-m-d H:i:s', strtotime($breakStart) + (30 * 60));
|
||||
$pdo->commit();
|
||||
successResponse(null, 'Příchod zaznamenán');
|
||||
} elseif ($ongoingShift) {
|
||||
switch ($action) {
|
||||
case 'break_start':
|
||||
if ($ongoingShift['arrival_time'] && !$ongoingShift['break_start']) {
|
||||
$breakStart = roundToNearest10Minutes($rawNow);
|
||||
$breakEnd = date('Y-m-d H:i:s', strtotime($breakStart) + (30 * 60));
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?');
|
||||
$stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]);
|
||||
$stmt = $pdo->prepare('UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?');
|
||||
$stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]);
|
||||
|
||||
successResponse(null, 'Pauza zaznamenána');
|
||||
} else {
|
||||
errorResponse('Nelze zadat pauzu');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'departure':
|
||||
if ($ongoingShift['arrival_time'] && !$ongoingShift['departure_time']) {
|
||||
$now = roundDownTo15Minutes($rawNow);
|
||||
|
||||
// Auto-add break if shift is longer than 6h and no break
|
||||
if (!$ongoingShift['break_start'] && !$ongoingShift['break_end']) {
|
||||
$arrivalTime = strtotime($ongoingShift['arrival_time']);
|
||||
$departureTime = strtotime($now);
|
||||
$hoursWorked = ($departureTime - $arrivalTime) / 3600;
|
||||
|
||||
if ($hoursWorked > 12) {
|
||||
$midPoint = $arrivalTime + (($departureTime - $arrivalTime) / 2);
|
||||
$breakStart = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint - (30 * 60)));
|
||||
$breakEnd = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint + (30 * 60)));
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?');
|
||||
$stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]);
|
||||
} elseif ($hoursWorked > 6) {
|
||||
$midPoint = $arrivalTime + (($departureTime - $arrivalTime) / 2);
|
||||
$breakStart = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint - (15 * 60)));
|
||||
$breakEnd = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint + (15 * 60)));
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?');
|
||||
$stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]);
|
||||
}
|
||||
$pdo->commit();
|
||||
successResponse(null, 'Pauza zaznamenána');
|
||||
} else {
|
||||
$pdo->rollBack();
|
||||
errorResponse('Nelze zadat pauzu');
|
||||
}
|
||||
break;
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE attendance
|
||||
SET departure_time = ?, departure_lat = ?, departure_lng = ?,
|
||||
departure_accuracy = ?, departure_address = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$now, $lat, $lng, $accuracy, $address, $ongoingShift['id']]);
|
||||
case 'departure':
|
||||
if ($ongoingShift['arrival_time'] && !$ongoingShift['departure_time']) {
|
||||
$now = roundDownTo15Minutes($rawNow);
|
||||
|
||||
// Close any open project log
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE attendance_project_logs SET ended_at = ? WHERE attendance_id = ? AND ended_at IS NULL
|
||||
');
|
||||
$stmt->execute([$now, $ongoingShift['id']]);
|
||||
// Auto-add break if shift is longer than 6h and no break
|
||||
if (!$ongoingShift['break_start'] && !$ongoingShift['break_end']) {
|
||||
$arrivalTime = strtotime($ongoingShift['arrival_time']);
|
||||
$departureTime = strtotime($now);
|
||||
$hoursWorked = ($departureTime - $arrivalTime) / 3600;
|
||||
|
||||
AuditLog::logUpdate('attendance', $ongoingShift['id'], [], [
|
||||
'departure_time' => $now,
|
||||
'location' => $address,
|
||||
], 'Odchod zaznamenán');
|
||||
if ($hoursWorked > 12) {
|
||||
$midPoint = $arrivalTime + (($departureTime - $arrivalTime) / 2);
|
||||
$breakStart = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint - (30 * 60)));
|
||||
$breakEnd = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint + (30 * 60)));
|
||||
|
||||
successResponse(null, 'Odchod zaznamenán');
|
||||
} else {
|
||||
errorResponse('Nelze zadat odchod');
|
||||
}
|
||||
break;
|
||||
$sql = 'UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?';
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]);
|
||||
} elseif ($hoursWorked > 6) {
|
||||
$midPoint = $arrivalTime + (($departureTime - $arrivalTime) / 2);
|
||||
$breakStart = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint - (15 * 60)));
|
||||
$breakEnd = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint + (15 * 60)));
|
||||
|
||||
default:
|
||||
errorResponse('Neplatná akce');
|
||||
$sql = 'UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?';
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE attendance
|
||||
SET departure_time = ?, departure_lat = ?, departure_lng = ?,
|
||||
departure_accuracy = ?, departure_address = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$now, $lat, $lng, $accuracy, $address, $ongoingShift['id']]);
|
||||
|
||||
// Close any open project log
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE attendance_project_logs SET ended_at = ? WHERE attendance_id = ? AND ended_at IS NULL
|
||||
');
|
||||
$stmt->execute([$now, $ongoingShift['id']]);
|
||||
|
||||
AuditLog::logUpdate('attendance', $ongoingShift['id'], [], [
|
||||
'departure_time' => $now,
|
||||
'location' => $address,
|
||||
], 'Odchod zaznamenán');
|
||||
|
||||
$pdo->commit();
|
||||
successResponse(null, 'Odchod zaznamenán');
|
||||
} else {
|
||||
$pdo->rollBack();
|
||||
errorResponse('Nelze zadat odchod');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$pdo->rollBack();
|
||||
errorResponse('Neplatná akce');
|
||||
}
|
||||
} else {
|
||||
$pdo->rollBack();
|
||||
errorResponse('Neplatná akce - nemáte aktivní směnu');
|
||||
}
|
||||
} else {
|
||||
errorResponse('Neplatná akce - nemáte aktivní směnu');
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -213,6 +213,8 @@ function handleGetList(PDO $pdo): void
|
||||
c.name as customer_name,
|
||||
(SELECT COALESCE(SUM(ii.quantity * ii.unit_price), 0)
|
||||
FROM invoice_items ii WHERE ii.invoice_id = i.id) as subtotal,
|
||||
(SELECT COALESCE(SUM(ii.quantity * ii.unit_price * ii.vat_rate / 100), 0)
|
||||
FROM invoice_items ii WHERE ii.invoice_id = i.id) as vat_amount,
|
||||
o.order_number
|
||||
{$from} {$where}
|
||||
ORDER BY {$p['sort']} {$p['order']}",
|
||||
@@ -222,20 +224,11 @@ function handleGetList(PDO $pdo): void
|
||||
|
||||
$invoices = $result['items'];
|
||||
|
||||
// Dopocitat celkovou castku s DPH
|
||||
foreach ($invoices as &$inv) {
|
||||
$subtotal = (float) $inv['subtotal'];
|
||||
if ($inv['apply_vat']) {
|
||||
$vatStmt = $pdo->prepare('
|
||||
SELECT COALESCE(SUM(quantity * unit_price * vat_rate / 100), 0)
|
||||
FROM invoice_items WHERE invoice_id = ?
|
||||
');
|
||||
$vatStmt->execute([$inv['id']]);
|
||||
$vatAmount = (float) $vatStmt->fetchColumn();
|
||||
$inv['total'] = $subtotal + $vatAmount;
|
||||
} else {
|
||||
$inv['total'] = $subtotal;
|
||||
}
|
||||
$inv['total'] = $inv['apply_vat']
|
||||
? $subtotal + (float) $inv['vat_amount']
|
||||
: $subtotal;
|
||||
}
|
||||
unset($inv);
|
||||
|
||||
|
||||
@@ -428,6 +428,7 @@ function handleDuplicate(PDO $pdo, int $sourceId): void
|
||||
'description' => $item['description'],
|
||||
'item_description' => $item['item_description'],
|
||||
'quantity' => $item['quantity'],
|
||||
'unit' => $item['unit'] ?? '',
|
||||
'unit_price' => $item['unit_price'],
|
||||
'is_included_in_total' => $item['is_included_in_total'],
|
||||
'position' => $item['position'],
|
||||
|
||||
@@ -34,10 +34,10 @@ try {
|
||||
$pdo = db();
|
||||
$userId = $authData['user_id'];
|
||||
|
||||
// Get existing user
|
||||
// Get existing user (vcetne password_hash pro overeni aktualniho hesla)
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT id, username, email, first_name, last_name, role_id, is_active,
|
||||
last_login, created_at
|
||||
last_login, created_at, password_hash
|
||||
FROM users WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$userId]);
|
||||
@@ -75,6 +75,14 @@ try {
|
||||
|
||||
// Update user
|
||||
if (!empty($input['password'])) {
|
||||
// Overeni aktualniho hesla
|
||||
if (empty($input['current_password'])) {
|
||||
errorResponse('Pro změnu hesla je nutné zadat aktuální heslo');
|
||||
}
|
||||
if (!password_verify($input['current_password'], $existingUser['password_hash'])) {
|
||||
errorResponse('Aktuální heslo není správné');
|
||||
}
|
||||
|
||||
// Validate password length
|
||||
if (strlen($input['password']) < 8) {
|
||||
errorResponse('Heslo musí mít alespoň 8 znaků');
|
||||
|
||||
@@ -579,27 +579,34 @@ function handleUpdateAttendance(PDO $pdo, int $recordId): void
|
||||
|
||||
$projectLogs = $input['project_logs'] ?? null;
|
||||
if ($projectLogs !== null) {
|
||||
$stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?');
|
||||
$stmt->execute([$recordId]);
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?');
|
||||
$stmt->execute([$recordId]);
|
||||
|
||||
if (!empty($projectLogs) && ($input['leave_type'] ?? 'work') === 'work') {
|
||||
$logStmt = $pdo->prepare(
|
||||
'INSERT INTO attendance_project_logs
|
||||
(attendance_id, project_id, hours, minutes)
|
||||
VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
foreach ($projectLogs as $log) {
|
||||
$pid = (int)($log['project_id'] ?? 0);
|
||||
if (!$pid) {
|
||||
continue;
|
||||
if (!empty($projectLogs) && ($input['leave_type'] ?? 'work') === 'work') {
|
||||
$logStmt = $pdo->prepare(
|
||||
'INSERT INTO attendance_project_logs
|
||||
(attendance_id, project_id, hours, minutes)
|
||||
VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
foreach ($projectLogs as $log) {
|
||||
$pid = (int)($log['project_id'] ?? 0);
|
||||
if (!$pid) {
|
||||
continue;
|
||||
}
|
||||
$h = (int)($log['hours'] ?? 0);
|
||||
$m = (int)($log['minutes'] ?? 0);
|
||||
if ($h === 0 && $m === 0) {
|
||||
continue;
|
||||
}
|
||||
$logStmt->execute([$recordId, $pid, $h, $m]);
|
||||
}
|
||||
$h = (int)($log['hours'] ?? 0);
|
||||
$m = (int)($log['minutes'] ?? 0);
|
||||
if ($h === 0 && $m === 0) {
|
||||
continue;
|
||||
}
|
||||
$logStmt->execute([$recordId, $pid, $h, $m]);
|
||||
}
|
||||
$pdo->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,18 +628,26 @@ function handleDeleteAttendance(PDO $pdo, int $recordId): void
|
||||
errorResponse('Záznam nebyl nalezen', 404);
|
||||
}
|
||||
|
||||
$leaveType = $record['leave_type'] ?? 'work';
|
||||
$leaveHours = $record['leave_hours'] ?? 0;
|
||||
if ($leaveType !== 'work' && $leaveHours > 0) {
|
||||
updateLeaveBalance($pdo, (int)$record['user_id'], $record['shift_date'], $leaveType, -(float)$leaveHours);
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$leaveType = $record['leave_type'] ?? 'work';
|
||||
$leaveHours = $record['leave_hours'] ?? 0;
|
||||
if ($leaveType !== 'work' && $leaveHours > 0) {
|
||||
updateLeaveBalance($pdo, (int)$record['user_id'], $record['shift_date'], $leaveType, -(float)$leaveHours);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?');
|
||||
$stmt->execute([$recordId]);
|
||||
|
||||
$stmt = $pdo->prepare('DELETE FROM attendance WHERE id = ?');
|
||||
$stmt->execute([$recordId]);
|
||||
|
||||
$pdo->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?');
|
||||
$stmt->execute([$recordId]);
|
||||
|
||||
$stmt = $pdo->prepare('DELETE FROM attendance WHERE id = ?');
|
||||
$stmt->execute([$recordId]);
|
||||
|
||||
AuditLog::logDelete('attendance', $recordId, $record, 'Admin smazal záznam docházky');
|
||||
|
||||
successResponse(null, 'Záznam byl smazán');
|
||||
|
||||
@@ -560,12 +560,12 @@ class NasFileManager
|
||||
return false;
|
||||
}
|
||||
|
||||
$attr = @exec('fsutil reparsepoint query "' . str_replace('/', '\\', $path) . '" 2>NUL');
|
||||
if ($attr !== false && $attr !== '') {
|
||||
// PHP is_link detekuje symlinky
|
||||
if (is_link($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback - realpath se lisi od puvodniho path u junction
|
||||
// Junction detekce pres porovnani realpath vs zadana cesta
|
||||
$real = realpath($path);
|
||||
$normalized = str_replace('\\', '/', $path);
|
||||
$normalReal = str_replace('\\', '/', (string) $real);
|
||||
|
||||
Reference in New Issue
Block a user