From 996f6f0371a6ff3dec125d45356b686115b49b5d Mon Sep 17 00:00:00 2001 From: jwed Date: Fri, 22 May 2026 19:55:04 -0400 Subject: [PATCH] fix(api): try UniFi OS Integration API first for X-API-Key auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /api/self/sites and /proxy/network/api/self/sites endpoints belong to the legacy session-cookie API — they don't accept X-API-Key auth and return 401 for keys generated in UniFi OS → Control Plane → Integrations. Adds /proxy/network/integration/v1/sites as the first endpoint tried, which is the actual home of API keys. Integration response rows look like { id, internalReference, name }; getSites normalizes them to the legacy { name, desc } shape using internalReference as the slug so downstream URLs (which build paths from $this->site) keep working. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Services/UnifiApiClient.php | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Services/UnifiApiClient.php b/src/Services/UnifiApiClient.php index af7a34f..d997136 100644 --- a/src/Services/UnifiApiClient.php +++ b/src/Services/UnifiApiClient.php @@ -625,8 +625,14 @@ class UnifiApiClient $http = $this->buildRequest(); } - // Try multiple URL patterns + // Endpoints, in preferred order: + // 1. UniFi OS Integration API — the one X-API-Key keys are issued for + // (response shape: [{ id, internalReference, name }]) + // 2. Legacy /proxy/network/api/self/sites — session-cookie API on + // UniFi OS consoles (response: [{ name, desc, ... }]) + // 3. Legacy /api/self/sites — standalone controller (no /proxy prefix) $paths = [ + '/proxy/network/integration/v1/sites', '/proxy/network/api/self/sites', '/api/self/sites', ]; @@ -645,11 +651,28 @@ class UnifiApiClient } $data = $response->json('data', $response->json()); - if (is_array($data) && ! empty($data) && isset($data[0]['name'])) { + if (! is_array($data) || empty($data)) { + $lastError = "No sites in response from {$path}"; + continue; + } + + // Integration API rows have `internalReference` (== legacy + // site slug) and `name` (human-readable). Normalize to + // the legacy {name, desc} shape so downstream code that + // builds URLs with the site slug keeps working. + if (isset($data[0]['internalReference'])) { + $data = array_map(fn ($s) => [ + 'name' => $s['internalReference'] ?? 'default', + 'desc' => $s['name'] ?? $s['internalReference'] ?? 'Default', + ], $data); + } + + if (isset($data[0]['name'])) { Log::debug('unifi.sites_found', ['path' => $path, 'count' => count($data)]); return $data; } - $lastError = "No sites in response from {$path}"; + + $lastError = "Unexpected response shape from {$path}"; } else { $lastError = "HTTP {$response->status()} on {$path}"; }