getConnection()->prepare( 'INSERT INTO audit_log (actor, action, table_name, record_id, old_data, new_data) VALUES (?, ?, ?, ?, ?, ?)' ); $stmt->execute([ $actor, $action, $tableName, $recordId, $oldData !== null ? self::safeJsonEncode($oldData) : null, $newData !== null ? self::safeJsonEncode($newData) : null, ]); } catch (\Throwable $e) { // Audit logging is best-effort — never crash the app over it. error_log('[Audit] write failed: ' . $e->getMessage()); } } /** * Build the actor string from the current request context. */ public static function actor(): string { $ip = $_SERVER['REMOTE_ADDR'] ?? 'cli'; $user = $_SESSION['admin_user'] ?? null; return $user ? "$user@$ip" : $ip; } /** * JSON-encode data, redacting sensitive/password fields. */ private static function safeJsonEncode(array $data): string { $safe = $data; // Redact password-like fields foreach (['password', 'pass', 'secret', 'token', 'credential'] as $key) { if (array_key_exists($key, $safe)) { $safe[$key] = '[REDACTED]'; } } return json_encode($safe, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR); } }