<?php declare(strict_types=1); if (!defined('DASHBOARD_ROOT')) { http_response_code(403); exit('Direct access denied.'); } class AuditLogger { public static function log( string $action, ?string $targetType = null, ?int $targetId = null, array $details = [] ): int { $adminId = $_SESSION['admin_id'] ?? null; try { return Database::insert( "INSERT INTO events (admin_id, action, target_type, target_id, fields_accessed, ip_address, user_agent, created_at)                  VALUES (:aid, :act, :tt, :tid, :fa, :ip, :ua, NOW())", [ ':aid' => $adminId, ':act' => $action, ':tt' => $targetType, ':tid' => $targetId, ':fa' => json_encode($details, JSON_UNESCAPED_UNICODE), ':ip' => $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0', ':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255), ] ); } catch (\Throwable $e) { error_log('[AuditLogger] ' . $e->getMessage()); return 0; } } public static function logWithSnapshot( string $action, ?string $targetType, ?int $targetId, ?string $snapshotBefore, ?string $snapshotAfter, ?int $adminId = null ): int { $adminId = $adminId ?? ($_SESSION['admin_id'] ?? null); $data = ['before' => $snapshotBefore, 'after' => $snapshotAfter]; try { return Database::insert( "INSERT INTO events (admin_id, action, target_type, target_id, fields_accessed, ip_address, user_agent, created_at)                  VALUES (:aid, :act, :tt, :tid, :fa, :ip, :ua, NOW())", [ ':aid' => $adminId, ':act' => $action, ':tt' => $targetType, ':tid' => $targetId, ':fa' => json_encode($data, JSON_UNESCAPED_UNICODE), ':ip' => $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0', ':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255), ] ); } catch (\Throwable $e) { error_log('[AuditLogger] ' . $e->getMessage()); return 0; } } public static function logDecrypt(int $customerId, string $reason, array $fieldsAccessed): int { return self::log('pii.decrypt', 'customer', $customerId, [ 'reason' => $reason, 'fields_accessed' => $fieldsAccessed, 'view_once' => true, ]); } public static function query(array $filters = [], int $limit = 100, int $offset = 0): array { $where = ['1=1']; $params = [':lim' => $limit, ':off' => $offset]; if (!empty($filters['admin_id'])) { $where[] = 'admin_id = :faid'; $params[':faid'] = $filters['admin_id']; } if (!empty($filters['action'])) { $where[] = 'action LIKE :fact'; $params[':fact'] = '%' . $filters['action'] . '%'; } if (!empty($filters['target_type'])) { $where[] = 'target_type = :ftt'; $params[':ftt'] = $filters['target_type']; } if (!empty($filters['since'])) { $where[] = 'created_at >= :since'; $params[':since'] = $filters['since']; } $whereClause = implode(' AND ', $where); return Database::fetchAll( "SELECT id, admin_id, action, target_type, target_id, fields_accessed, ip_address, created_at              FROM events WHERE {$whereClause}              ORDER BY id DESC LIMIT :lim OFFSET :off", $params ); } public static function count(array $filters = []): int { $where = ['1=1']; $params = []; if (!empty($filters['admin_id'])) { $where[] = 'admin_id = :faid'; $params[':faid'] = $filters['admin_id']; } if (!empty($filters['action'])) { $where[] = 'action LIKE :fact'; $params[':fact'] = '%' . $filters['action'] . '%'; } $whereClause = implode(' AND ', $where); return (int) Database::scalar( "SELECT COUNT(*) FROM events WHERE {$whereClause}", $params ); } } 