|null $oldValues Previous values (for updates) * @param array|null $newValues New values (for updates/creates) */ public static function log( string $action, ?string $entityType = null, ?int $entityId = null, ?string $description = null, ?array $oldValues = null, ?array $newValues = null ): void { try { $pdo = db(); $userId = self::$currentUserId; $username = self::$currentUsername; $stmt = $pdo->prepare(' INSERT INTO audit_logs ( user_id, username, user_ip, action, entity_type, entity_id, description, old_values, new_values, user_agent, session_id ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '); $stmt->execute([ $userId, $username, getClientIp(), $action, $entityType, $entityId, $description, $oldValues ? json_encode($oldValues, JSON_UNESCAPED_UNICODE) : null, $newValues ? json_encode($newValues, JSON_UNESCAPED_UNICODE) : null, substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 500), session_id() ?: null, ]); } catch (PDOException $e) { error_log('AuditLog error: ' . $e->getMessage()); } } /** * Log successful login * * @param int $userId User ID * @param string $username Username */ public static function logLogin(int $userId, string $username): void { try { $pdo = db(); $stmt = $pdo->prepare(' INSERT INTO audit_logs ( user_id, username, user_ip, action, entity_type, entity_id, description, user_agent, session_id ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) '); $stmt->execute([ $userId, $username, getClientIp(), self::ACTION_LOGIN, 'user', $userId, "Přihlášení uživatele '$username'", substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 500), session_id() ?: null, ]); } catch (PDOException $e) { error_log('AuditLog login error: ' . $e->getMessage()); } } /** * Log failed login attempt * * @param string $username Attempted username * @param string $reason Failure reason */ public static function logLoginFailed(string $username, string $reason = 'invalid_credentials'): void { try { $pdo = db(); $stmt = $pdo->prepare(' INSERT INTO audit_logs ( username, user_ip, action, entity_type, description, user_agent, session_id ) VALUES (?, ?, ?, ?, ?, ?, ?) '); $stmt->execute([ $username, getClientIp(), self::ACTION_LOGIN_FAILED, 'user', "Neúspěšné přihlášení '$username': $reason", substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 500), session_id() ?: null, ]); } catch (PDOException $e) { error_log('AuditLog login failed error: ' . $e->getMessage()); } } /** * Log logout * * @param int|null $userId User ID (optional, for JWT-based auth) * @param string|null $username Username (optional, for JWT-based auth) */ public static function logLogout(?int $userId = null, ?string $username = null): void { if ($userId === null) { return; } try { $pdo = db(); $stmt = $pdo->prepare(' INSERT INTO audit_logs ( user_id, username, user_ip, action, entity_type, entity_id, description, user_agent ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) '); $stmt->execute([ $userId, $username, getClientIp(), self::ACTION_LOGOUT, 'user', $userId, "Odhlášení uživatele '{$username}'", substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 500), ]); } catch (PDOException $e) { error_log('AuditLog logout error: ' . $e->getMessage()); } } /** * Log entity creation * * @param string $entityType Entity type * @param int $entityId Entity ID * @param array $data Created data * @param string|null $description Optional description */ public static function logCreate( string $entityType, int $entityId, array $data, ?string $description = null ): void { // Remove sensitive fields from logged data $safeData = self::sanitizeData($data); self::log( self::ACTION_CREATE, $entityType, $entityId, $description ?? "Vytvořen $entityType #$entityId", null, $safeData ); } /** * Log entity update * * @param string $entityType Entity type * @param int $entityId Entity ID * @param array $oldData Old values * @param array $newData New values * @param string|null $description Optional description */ public static function logUpdate( string $entityType, int $entityId, array $oldData, array $newData, ?string $description = null ): void { // Only log changed fields $changes = self::getChanges($oldData, $newData); if (empty($changes['old']) && empty($changes['new'])) { return; // No actual changes } self::log( self::ACTION_UPDATE, $entityType, $entityId, $description ?? "Upraven $entityType #$entityId", $changes['old'], $changes['new'] ); } /** * Log entity deletion * * @param string $entityType Entity type * @param int $entityId Entity ID * @param array|null $data Deleted entity data * @param string|null $description Optional description */ public static function logDelete( string $entityType, int $entityId, ?array $data = null, ?string $description = null ): void { $safeData = $data ? self::sanitizeData($data) : null; self::log( self::ACTION_DELETE, $entityType, $entityId, $description ?? "Smazán $entityType #$entityId", $safeData, null ); } /** * Log access denied * * @param string $resource Resource that was denied * @param string|null $permission Required permission */ public static function logAccessDenied(string $resource, ?string $permission = null): void { $description = "Přístup odepřen k '$resource'"; if ($permission) { $description .= " (vyžaduje: $permission)"; } self::log( self::ACTION_ACCESS_DENIED, null, null, $description ); } /** * Get changes between old and new data * * @param array $oldData Old values * @param array $newData New values * @return array{old: array, new: array} */ private static function getChanges(array $oldData, array $newData): array { $oldData = self::sanitizeData($oldData); $newData = self::sanitizeData($newData); $changedOld = []; $changedNew = []; // Find changed fields foreach ($newData as $key => $newValue) { $oldValue = $oldData[$key] ?? null; if ($oldValue !== $newValue) { $changedOld[$key] = $oldValue; $changedNew[$key] = $newValue; } } // Find removed fields foreach ($oldData as $key => $oldValue) { if (!array_key_exists($key, $newData)) { $changedOld[$key] = $oldValue; $changedNew[$key] = null; } } return ['old' => $changedOld, 'new' => $changedNew]; } /** * Remove sensitive fields from data before logging * * @param array $data Data to sanitize * @return array Sanitized data */ private static function sanitizeData(array $data): array { $sensitiveFields = [ 'password', 'password_hash', 'token', 'token_hash', 'secret', 'api_key', 'private_key', 'csrf_token', ]; foreach ($sensitiveFields as $field) { if (isset($data[$field])) { $data[$field] = '[REDACTED]'; } } return $data; } /** * Get audit logs with filtering and pagination * * @param array $filters Filter options * @param int $page Page number (1-based) * @param int $perPage Items per page * @return array{logs: list>, total: int, pages: int, page: int, per_page: int} */ public static function getLogs(array $filters = [], int $page = 1, int $perPage = 50): array { try { $pdo = db(); $where = []; $params = []; // Apply filters if (!empty($filters['user_id'])) { $where[] = 'user_id = ?'; $params[] = $filters['user_id']; } if (!empty($filters['username'])) { $where[] = 'username LIKE ?'; $params[] = '%' . $filters['username'] . '%'; } if (!empty($filters['action'])) { $where[] = 'action = ?'; $params[] = $filters['action']; } if (!empty($filters['entity_type'])) { $where[] = 'entity_type = ?'; $params[] = $filters['entity_type']; } if (!empty($filters['ip'])) { $where[] = 'user_ip LIKE ?'; $params[] = '%' . $filters['ip'] . '%'; } if (!empty($filters['date_from'])) { $where[] = 'created_at >= ?'; $params[] = $filters['date_from'] . ' 00:00:00'; } if (!empty($filters['date_to'])) { $where[] = 'created_at <= ?'; $params[] = $filters['date_to'] . ' 23:59:59'; } if (!empty($filters['search'])) { $where[] = '(description LIKE ? OR username LIKE ?)'; $searchTerm = '%' . $filters['search'] . '%'; $params[] = $searchTerm; $params[] = $searchTerm; } $whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; // Count total $countSql = "SELECT COUNT(*) FROM audit_logs $whereClause"; $stmt = $pdo->prepare($countSql); $stmt->execute($params); $total = (int) $stmt->fetchColumn(); // Calculate pagination $pages = max(1, ceil($total / $perPage)); $page = max(1, min($page, $pages)); $offset = ($page - 1) * $perPage; // Get logs $sql = " SELECT id, user_id, username, user_ip, action, entity_type, entity_id, description, old_values, new_values, created_at FROM audit_logs $whereClause ORDER BY created_at DESC LIMIT $perPage OFFSET $offset "; $stmt = $pdo->prepare($sql); $stmt->execute($params); $logs = $stmt->fetchAll(); // Parse JSON fields foreach ($logs as &$log) { $log['old_values'] = $log['old_values'] ? json_decode($log['old_values'], true) : null; $log['new_values'] = $log['new_values'] ? json_decode($log['new_values'], true) : null; } return [ 'logs' => $logs, 'total' => $total, 'pages' => $pages, 'page' => $page, 'per_page' => $perPage, ]; } catch (PDOException $e) { error_log('AuditLog getLogs error: ' . $e->getMessage()); return ['logs' => [], 'total' => 0, 'pages' => 0, 'page' => 1, 'per_page' => $perPage]; } } /** * Get recent activity for a user * * @param int $userId User ID * @param int $limit Number of records * @return list> Recent logs */ public static function getUserActivity(int $userId, int $limit = 10): array { try { $pdo = db(); $stmt = $pdo->prepare(' SELECT id, user_id, username, user_ip, action, entity_type, entity_id, description, old_values, new_values, created_at FROM audit_logs WHERE user_id = ? ORDER BY created_at DESC LIMIT ? '); $stmt->execute([$userId, $limit]); return $stmt->fetchAll(); } catch (PDOException $e) { error_log('AuditLog getUserActivity error: ' . $e->getMessage()); return []; } } /** * Get entity history * * @param string $entityType Entity type * @param int $entityId Entity ID * @return list> Audit log history */ public static function getEntityHistory(string $entityType, int $entityId): array { try { $pdo = db(); $stmt = $pdo->prepare(' SELECT id, user_id, username, user_ip, action, entity_type, entity_id, description, old_values, new_values, created_at FROM audit_logs WHERE entity_type = ? AND entity_id = ? ORDER BY created_at DESC '); $stmt->execute([$entityType, $entityId]); $logs = $stmt->fetchAll(); foreach ($logs as &$log) { $log['old_values'] = $log['old_values'] ? json_decode($log['old_values'], true) : null; $log['new_values'] = $log['new_values'] ? json_decode($log['new_values'], true) : null; } return $logs; } catch (PDOException $e) { error_log('AuditLog getEntityHistory error: ' . $e->getMessage()); return []; } } }