Initial commit
This commit is contained in:
556
api/includes/AuditLog.php
Normal file
556
api/includes/AuditLog.php
Normal file
@@ -0,0 +1,556 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* BOHA Automation - Audit Logging System
|
||||
*
|
||||
* Comprehensive audit trail for all administrative actions
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__) . '/config.php';
|
||||
|
||||
class AuditLog
|
||||
{
|
||||
// Action types
|
||||
public const ACTION_LOGIN = 'login';
|
||||
public const ACTION_LOGIN_FAILED = 'login_failed';
|
||||
public const ACTION_LOGOUT = 'logout';
|
||||
public const ACTION_CREATE = 'create';
|
||||
public const ACTION_UPDATE = 'update';
|
||||
public const ACTION_DELETE = 'delete';
|
||||
public const ACTION_VIEW = 'view';
|
||||
public const ACTION_ACTIVATE = 'activate';
|
||||
public const ACTION_DEACTIVATE = 'deactivate';
|
||||
public const ACTION_PASSWORD_CHANGE = 'password_change';
|
||||
public const ACTION_PERMISSION_CHANGE = 'permission_change';
|
||||
public const ACTION_ACCESS_DENIED = 'access_denied';
|
||||
|
||||
private static ?int $currentUserId = null;
|
||||
private static ?string $currentUsername = null;
|
||||
|
||||
/**
|
||||
* Nastaví kontext aktuálního uživatele pro všechny následující logy
|
||||
*/
|
||||
public static function setUser(int $userId, string $username): void
|
||||
{
|
||||
self::$currentUserId = $userId;
|
||||
self::$currentUsername = $username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an action
|
||||
*
|
||||
* @param string $action Action type (use class constants)
|
||||
* @param string|null $entityType Entity type (e.g., 'user', 'project')
|
||||
* @param int|null $entityId Entity ID
|
||||
* @param string|null $description Human-readable description
|
||||
* @param array<string, mixed>|null $oldValues Previous values (for updates)
|
||||
* @param array<string, mixed>|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<string, mixed> $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<string, mixed> $oldData Old values
|
||||
* @param array<string, mixed> $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<string, mixed>|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<string, mixed> $oldData Old values
|
||||
* @param array<string, mixed> $newData New values
|
||||
* @return array{old: array<string, mixed>, new: array<string, mixed>}
|
||||
*/
|
||||
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<string, mixed> $data Data to sanitize
|
||||
* @return array<string, mixed> 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<string, mixed> $filters Filter options
|
||||
* @param int $page Page number (1-based)
|
||||
* @param int $perPage Items per page
|
||||
* @return array{logs: list<array<string, mixed>>, 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 *
|
||||
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<array<string, mixed>> Recent logs
|
||||
*/
|
||||
public static function getUserActivity(int $userId, int $limit = 10): array
|
||||
{
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT *
|
||||
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<array<string, mixed>> Audit log history
|
||||
*/
|
||||
public static function getEntityHistory(string $entityType, int $entityId): array
|
||||
{
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT *
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user