connect($mikrotik); $identity = $client->query('/system/identity/print')->read()[0] ?? []; $resource = $client->query('/system/resource/print')->read()[0] ?? []; return [ 'connected' => true, 'latency_ms' => (int) round((hrtime(true) - $startedAt) / 1_000_000), 'identity' => $identity['name'] ?? null, 'version' => $resource['version'] ?? null, 'uptime' => $resource['uptime'] ?? null, 'platform' => $resource['platform'] ?? null, 'board_name' => $resource['board-name'] ?? null, ]; } public function createAccount(NasMikrotik $mikrotik, string $serviceType, array $attributes): array { $client = $this->connect($mikrotik); $this->assertUsernameAvailable($client, $serviceType, $attributes['username']); $query = new Query($this->path($serviceType, 'add')); foreach ($this->accountAttributes($attributes) as $key => $value) { $query->equal($key, $value); } $client->query($query)->read(); return $this->findAccount($client, $serviceType, $attributes['username']); } public function updateAccount(NasMikrotik $mikrotik, string $serviceType, string $username, array $attributes): array { $client = $this->connect($mikrotik); $account = $this->findAccount($client, $serviceType, $username); $query = (new Query($this->path($serviceType, 'set')))->equal('.id', $account['.id']); foreach ($this->accountAttributes($attributes) as $key => $value) { $query->equal($key, $value); } $client->query($query)->read(); return $this->findAccount($client, $serviceType, $attributes['username'] ?? $username); } public function setEnabled(NasMikrotik $mikrotik, string $serviceType, string $username, bool $enabled): array { return $this->updateAccount($mikrotik, $serviceType, $username, [ 'disabled' => $enabled ? 'no' : 'yes', ]); } public function deleteAccount(NasMikrotik $mikrotik, string $serviceType, string $username): void { $client = $this->connect($mikrotik); $account = $this->findAccount($client, $serviceType, $username); $client->query( (new Query($this->path($serviceType, 'remove')))->equal('.id', $account['.id']) )->read(); } public function syncProfile(NasMikrotik $mikrotik, string $serviceType, string $name, array $attributes, string $mode = 'update'): array { $client = $this->connect($mikrotik); $base = $serviceType === 'hotspot' ? '/ip/hotspot/user/profile' : '/ppp/profile'; $rows = $client->query((new Query("{$base}/print"))->where('name', $name))->read(); $existing = $rows[0] ?? null; if ($mode === 'adopt') { if (! $existing) throw ValidationException::withMessages(['profile' => "Profile {$name} tidak ditemukan untuk di-adopt."]); return $existing; } if (! $existing) { $query = (new Query("{$base}/add"))->equal('name', $name); } else { $query = (new Query("{$base}/set"))->equal('.id', $existing['.id']); } foreach (array_filter($attributes, fn ($value) => $value !== null) as $key => $value) $query->equal($key, $value); $client->query($query)->read(); $rows = $client->query((new Query("{$base}/print"))->where('name', $name))->read(); return $rows[0] ?? []; } public function inspectAccount(NasMikrotik $mikrotik, string $serviceType, string $username): array { $client = $this->connect($mikrotik); $account = $this->findAccount($client, $serviceType, $username); $activeBase = in_array($serviceType, ['ppp','pppoe'], true) ? '/ppp/active' : '/ip/hotspot/active'; $active = $client->query((new Query("{$activeBase}/print"))->where('name', $username))->read()[0] ?? null; return ['account' => $account, 'active' => $active]; } public function disconnectAccount(NasMikrotik $mikrotik, string $serviceType, string $username): void { $client = $this->connect($mikrotik); $activeBase = in_array($serviceType, ['ppp','pppoe'], true) ? '/ppp/active' : '/ip/hotspot/active'; $rows = $client->query((new Query("{$activeBase}/print"))->where('name', $username))->read(); foreach ($rows as $row) if (! empty($row['.id'])) $client->query((new Query("{$activeBase}/remove"))->equal('.id', $row['.id']))->read(); } private function connect(NasMikrotik $mikrotik): Client { if ($mikrotik->connection_type !== 'api') { throw ValidationException::withMessages(['mikrotik' => 'Perangkat tidak menggunakan koneksi RouterOS API.']); } if ($mikrotik->status !== 'active') { throw ValidationException::withMessages(['mikrotik' => 'Perangkat Mikrotik sedang nonaktif.']); } if (! $mikrotik->host || ! $mikrotik->api_username || ! $mikrotik->api_password) { throw ValidationException::withMessages(['mikrotik' => 'Host, username API, dan password API wajib lengkap.']); } try { return new Client([ 'host' => $mikrotik->host, 'user' => $mikrotik->api_username, 'pass' => $mikrotik->api_password, 'port' => (int) ($mikrotik->api_port ?: 8728), 'timeout' => (int) ($mikrotik->timeout ?: 10), 'socket_timeout' => (int) ($mikrotik->timeout ?: 10), 'attempts' => 1, 'delay' => 0, 'ssl' => (int) ($mikrotik->api_port ?: 8728) === 8729, ]); } catch (Throwable $exception) { throw ValidationException::withMessages([ 'mikrotik' => 'Koneksi RouterOS API gagal: '.$exception->getMessage(), ]); } } private function findAccount(Client $client, string $serviceType, string $username): array { $rows = $client->query( (new Query($this->path($serviceType, 'print')))->where('name', $username) )->read(); if (empty($rows[0]['.id'])) { throw ValidationException::withMessages(['username' => "Akun {$username} tidak ditemukan di Mikrotik."]); } return $rows[0]; } private function assertUsernameAvailable(Client $client, string $serviceType, string $username): void { $rows = $client->query( (new Query($this->path($serviceType, 'print')))->where('name', $username) )->read(); if ($rows) { throw ValidationException::withMessages(['username' => "Username {$username} sudah ada di Mikrotik."]); } } private function accountAttributes(array $attributes): array { return array_filter([ 'name' => $attributes['username'] ?? null, 'password' => $attributes['password'] ?? null, 'profile' => $attributes['profile'] ?? null, 'comment' => $attributes['comment'] ?? null, 'disabled' => $attributes['disabled'] ?? null, ], fn ($value) => $value !== null); } private function path(string $serviceType, string $action): string { $base = match ($serviceType) { 'ppp', 'pppoe' => '/ppp/secret', 'hotspot' => '/ip/hotspot/user', default => throw ValidationException::withMessages(['service_type' => 'Tipe layanan harus PPPoE atau Hotspot.']), }; return "{$base}/{$action}"; } }