'user', 'RADIQ-WRITE' => 'admin', 'RADIQ-NOC' => 'admin']; public function __construct( private HsgqCliClient $client, private readonly ?HsgqCliClient $writeFallback = null, private readonly ?HsgqWebClient $webFallback = null, ) {} public function testLogin(string $username, string $password, ?string $enablePassword = null): array { $this->client->connect($username, $password); try { $this->prepare($enablePassword ?: $password); return ['users' => $this->client->command('show user')]; } finally { $this->client->disconnect(); } } public function rotateRootPassword(string $currentPassword, string $newPassword, ?string $enablePassword = null): void { $this->validatePassword($newPassword); try { $this->withWriteFallback(function () use ($currentPassword, $newPassword, $enablePassword): void { $this->client->connect('root', $currentPassword); try { $this->prepare($enablePassword ?: $currentPassword); try { $this->client->command('user password-self '.$newPassword); } catch (\RuntimeException $exception) { if (! str_starts_with($exception->getMessage(), 'COMMAND_REJECTED')) { throw $exception; } $this->client->command('user password '.$newPassword); } $this->client->command('user save'); } finally { $this->client->disconnect(); } }); } catch (\Throwable $cliException) { if (! $this->webFallback) { throw $cliException; } $this->webFallback->login('root', $currentPassword); $this->webFallback->changePassword('root', $newPassword, $currentPassword); } $this->testLogin('root', $newPassword, $enablePassword === $currentPassword ? $newPassword : $enablePassword); } public function syncUser(string $rootPassword, ?string $enablePassword, string $username, string $password, string $group, bool $enabled): array { $this->validateAccount($username, $password); $role = self::ROLES[$group] ?? throw new InvalidArgumentException('HSGQ_ROLE_INVALID: group RADIQ tidak didukung HSGQ.'); try { return $this->withWriteFallback(function () use ($rootPassword, $enablePassword, $username, $password, $role, $enabled): array { $this->client->connect('root', $rootPassword); try { $this->prepare($enablePassword ?: $rootPassword); $users = $this->client->command('show user'); $exists = $this->userExists($users, $username); if (! $enabled) { if ($exists) { $this->removeUser($username); } return ['remote_id' => null]; } $exists ? $this->client->command("user password {$username} {$password}") : $this->client->command("user add {$username} {$password} {$role} reenter 4"); $this->client->command('user save'); if (! $this->userExists($this->client->command('show user'), $username)) { throw new \RuntimeException('COMMAND_NOT_APPLIED: user tidak ditemukan setelah sinkronisasi HSGQ.'); } return ['remote_id' => $username, 'role' => $role]; } finally { $this->client->disconnect(); } }); } catch (\Throwable $cliException) { return $this->syncUserViaWeb($rootPassword, $username, $password, $group, $enabled, $cliException); } } public function deleteUser(string $rootPassword, ?string $enablePassword, string $username): void { $this->validateUsername($username); try { $this->withWriteFallback(function () use ($rootPassword, $enablePassword, $username): void { $this->client->connect('root', $rootPassword); try { $this->prepare($enablePassword ?: $rootPassword); if ($this->userExists($this->client->command('show user'), $username)) { $this->removeUser($username); } } finally { $this->client->disconnect(); } }); } catch (\Throwable $cliException) { if (! $this->webFallback) { throw $cliException; } $this->webFallback->login('root', $rootPassword); if ($this->webFallback->hasUser($username)) { $this->webFallback->deleteUser($username); } } } private function removeUser(string $username): void { try { $this->client->command('user offline '.$username); } catch (\RuntimeException $exception) { if (! str_contains($exception->getMessage(), 'This account can not be process')) { throw $exception; } } $this->client->command('user delete '.$username); $this->client->command('user save'); } private function userExists(string $users, string $username): bool { return preg_match('/^\s*'.preg_quote($username, '/').'\s+/mi', $users) === 1; } private function withWriteFallback(Closure $operation): mixed { $primary = $this->client; try { return $operation(); } catch (\Throwable $primaryException) { if (! $this->writeFallback) { throw $primaryException; } $this->client = $this->writeFallback; try { return $operation(); } catch (\Throwable $fallbackException) { throw new \RuntimeException('HSGQ_DUAL_CONNECTION_FAILED: SSH gagal menerapkan perubahan dan Telnet gagal: '.str($fallbackException->getMessage())->after(':')->trim()->limit(140), 0, $fallbackException); } finally { $this->client = $primary; } } } private function syncUserViaWeb(string $rootPassword, string $username, string $password, string $group, bool $enabled, \Throwable $cliException): array { if (! $this->webFallback) { throw $cliException; } $level = match ($group) { 'RADIQ-READ' => 5, 'RADIQ-WRITE', 'RADIQ-NOC' => 3, default => throw new InvalidArgumentException('HSGQ_ROLE_INVALID: group RADIQ tidak didukung WebGUI HSGQ.'), }; $this->webFallback->login('root', $rootPassword); $exists = $this->webFallback->hasUser($username); if (! $enabled) { if ($exists) { $this->webFallback->deleteUser($username); } return ['remote_id' => null]; } if ($exists) { $this->webFallback->changePassword($username, $password); } else { $this->webFallback->addUser($username, $password, $level); } if (! $this->webFallback->hasUser($username)) { throw new \RuntimeException('COMMAND_NOT_APPLIED: user tidak ditemukan setelah sinkronisasi WebGUI HSGQ.'); } return ['remote_id' => $username, 'role' => $level, 'transport' => 'webgui']; } private function validateAccount(string $username, string $password): void { $this->validateUsername($username); $this->validatePassword($password); } private function prepare(string $enablePassword): void { $this->client->enterEnable($enablePassword); $this->client->command('terminal length 0'); $this->client->command('configure'); } private function validateUsername(string $username): void { if ($username === 'root' || ! preg_match('/^[A-Za-z0-9_]{4,16}$/', $username)) { throw new InvalidArgumentException('HSGQ_USERNAME_INVALID: username HSGQ harus 4-16 karakter atau merupakan akun yang dilindungi.'); } } private function validatePassword(string $password): void { if ($password === '' || strlen($password) > 64 || preg_match('/\s/', $password)) { throw new InvalidArgumentException('HSGQ_PASSWORD_INVALID: password HSGQ maksimal 64 karakter tanpa spasi.'); } } }