exec(" CREATE TABLE IF NOT EXISTS ac_audit_logs ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, actor_id INT UNSIGNED NULL, actor_name VARCHAR(120) NULL, actor_role VARCHAR(40) NULL, action VARCHAR(120) NOT NULL, context_json MEDIUMTEXT NULL, ip_address VARCHAR(45) NULL, user_agent VARCHAR(255) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; "); } catch (Throwable $e) { return; } } public static function log(string $action, array $context = []): void { $db = Database::get(); if (!($db instanceof PDO)) { return; } self::ensureTable(); try { $stmt = $db->prepare(" INSERT INTO ac_audit_logs (actor_id, actor_name, actor_role, action, context_json, ip_address, user_agent) VALUES (:actor_id, :actor_name, :actor_role, :action, :context_json, :ip_address, :user_agent) "); $stmt->execute([ ':actor_id' => Auth::id() > 0 ? Auth::id() : null, ':actor_name' => Auth::name() !== '' ? Auth::name() : null, ':actor_role' => Auth::role() !== '' ? Auth::role() : null, ':action' => $action, ':context_json' => $context ? json_encode($context, JSON_UNESCAPED_SLASHES) : null, ':ip_address' => self::ip(), ':user_agent' => mb_substr((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255), ]); } catch (Throwable $e) { return; } } public static function latest(int $limit = 100): array { $db = Database::get(); if (!($db instanceof PDO)) { return []; } self::ensureTable(); $limit = max(1, min(500, $limit)); try { $stmt = $db->prepare(" SELECT id, actor_name, actor_role, action, context_json, ip_address, created_at FROM ac_audit_logs ORDER BY id DESC LIMIT :limit "); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; } catch (Throwable $e) { return []; } } private static function ip(): ?string { $candidates = [ (string)($_SERVER['HTTP_CF_CONNECTING_IP'] ?? ''), (string)($_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''), (string)($_SERVER['REMOTE_ADDR'] ?? ''), ]; foreach ($candidates as $candidate) { if ($candidate === '') { continue; } $first = trim(explode(',', $candidate)[0] ?? ''); if ($first !== '' && filter_var($first, FILTER_VALIDATE_IP)) { return $first; } } return null; } }