Files
services_core/app/Services/Nas/RouterOsApiService.php
T
2026-08-07 09:16:13 +07:00

186 lines
7.8 KiB
PHP

<?php
namespace App\Services\Nas;
use App\Models\NasMikrotik;
use Illuminate\Validation\ValidationException;
use RouterOS\Client;
use RouterOS\Query;
use Throwable;
class RouterOsApiService
{
public function testConnection(NasMikrotik $mikrotik): array
{
$startedAt = hrtime(true);
$client = $this->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}";
}
}