diff --git a/composer.json b/composer.json index c766499..e170ae0 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "dashboard/unifi", "description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform", - "version": "1.9.0", + "version": "1.9.1", "type": "library", "license": "MIT", "autoload": { diff --git a/src/Console/RotatePasswords.php b/src/Console/RotatePasswords.php index 16ce305..8468b54 100644 --- a/src/Console/RotatePasswords.php +++ b/src/Console/RotatePasswords.php @@ -81,24 +81,22 @@ class RotatePasswords extends Command try { if (str_starts_with((string) $ppsk->unifi_id, 'emb_')) { // Embedded PPSK: update inside the parent WLAN object. - // Synthetic ID is derived from the new passphrase, so update it too. - $unifi->updateEmbeddedPpsk($ppsk->wlan_id, $ppsk->x_passphrase, $newPass); + // Match by name (most reliable) — falls back to + // passphrase if name is missing. + $unifi->updateEmbeddedPpsk($ppsk->wlan_id, $ppsk->x_passphrase, $newPass, $ppsk->name); $newUid = 'emb_' . substr(hash('sha256', $ppsk->wlan_id . ':' . $newPass), 0, 32); - $oldPass = $ppsk->x_passphrase; $ppsk->update(['x_passphrase' => $newPass, 'unifi_id' => $newUid]); // Sibling WLANs (same SSID name on a different band): - // their embedded PPSK with the same name also needs - // to rotate to the same new password so the SSID's - // 2.4/5GHz halves stay in sync. + // rotate the matching-name PPSK in each so the + // SSID's 2.4/5GHz halves stay in sync. foreach ($unifi->getWlanSiblings($ppsk->wlan_id) as $siblingWlanId) { $sibling = UnifiPpsk::where('wlan_id', $siblingWlanId) ->where('name', $ppsk->name) ->where('state', 'active') ->first(); - $siblingOldPass = $sibling?->x_passphrase ?? $oldPass; try { - $unifi->updateEmbeddedPpsk($siblingWlanId, $siblingOldPass, $newPass); + $unifi->updateEmbeddedPpsk($siblingWlanId, $sibling?->x_passphrase, $newPass, $ppsk->name); if ($sibling) { $sibling->update([ 'x_passphrase' => $newPass, diff --git a/src/Http/Controllers/WifiController.php b/src/Http/Controllers/WifiController.php index f4f6142..7ac9f66 100644 --- a/src/Http/Controllers/WifiController.php +++ b/src/Http/Controllers/WifiController.php @@ -293,9 +293,9 @@ class WifiController extends Controller if (! empty($unifiUpdate)) { if (str_starts_with($record->unifi_id, 'emb_') && isset($unifiUpdate['x_passphrase'])) { // Embedded PPSK update path — modify the WLAN's embedded array. + // Match by name (reliable across drift). $newPass = $unifiUpdate['x_passphrase']; - $oldPass = $record->x_passphrase; - $unifi->updateEmbeddedPpsk($record->wlan_id, $oldPass, $newPass); + $unifi->updateEmbeddedPpsk($record->wlan_id, $record->x_passphrase, $newPass, $record->name); $data['unifi_id'] = 'emb_' . substr(hash('sha256', $record->wlan_id . ':' . $newPass), 0, 32); // Also update sibling WLANs (banded SSID — same name @@ -305,9 +305,8 @@ class WifiController extends Controller ->where('name', $record->name) ->where('state', 'active') ->first(); - $siblingOldPass = $sibling?->x_passphrase ?? $oldPass; try { - $unifi->updateEmbeddedPpsk($siblingWlanId, $siblingOldPass, $newPass); + $unifi->updateEmbeddedPpsk($siblingWlanId, $sibling?->x_passphrase, $newPass, $record->name); if ($sibling) { $sibling->update([ 'x_passphrase' => $newPass, diff --git a/src/Services/UnifiApiClient.php b/src/Services/UnifiApiClient.php index 793a3e4..d9c7f7d 100644 --- a/src/Services/UnifiApiClient.php +++ b/src/Services/UnifiApiClient.php @@ -538,7 +538,7 @@ class UnifiApiClient * no controller-side ID. Only changes the entry's passphrase; name * isn't separately addressable on embedded PPSKs. */ - public function updateEmbeddedPpsk(string $wlanId, string $oldPassphrase, string $newPassphrase): array + public function updateEmbeddedPpsk(string $wlanId, ?string $oldPassphrase, string $newPassphrase, ?string $name = null): array { $wlanResp = $this->get("/rest/wlanconf/{$wlanId}"); $wlan = $wlanResp[0] ?? $wlanResp; @@ -548,26 +548,50 @@ class UnifiApiClient throw new \RuntimeException('WLAN has no embedded PPSKs to update.'); } - $matched = false; - foreach ($entries as &$e) { - $current = $e['x_passphrase'] ?? $e['password'] ?? $e['passphrase'] ?? null; - if ($current === $oldPassphrase) { - // Preserve whichever field name the controller is using. - if (array_key_exists('x_passphrase', $e)) $e['x_passphrase'] = $newPassphrase; - if (array_key_exists('password', $e)) $e['password'] = $newPassphrase; - if (array_key_exists('passphrase', $e)) $e['passphrase'] = $newPassphrase; - // If none of the above existed, default to password (most common on embedded). - if (! isset($e['x_passphrase']) && ! isset($e['password']) && ! isset($e['passphrase'])) { - $e['password'] = $newPassphrase; - } - $matched = true; - break; + // Match in this order — most reliable first: + // 1. by PPSK name (if provided) — survives passphrase drift + // caused by manual edits or previous out-of-sync rotations. + // 2. by current passphrase (legacy) + $applyUpdate = function (array &$e) use ($newPassphrase) { + if (array_key_exists('x_passphrase', $e)) $e['x_passphrase'] = $newPassphrase; + if (array_key_exists('password', $e)) $e['password'] = $newPassphrase; + if (array_key_exists('passphrase', $e)) $e['passphrase'] = $newPassphrase; + if (! isset($e['x_passphrase']) && ! isset($e['password']) && ! isset($e['passphrase'])) { + $e['password'] = $newPassphrase; } + }; + + $matched = false; + if ($name !== null && $name !== '') { + foreach ($entries as &$e) { + $entryName = $e['name'] ?? $e['label'] ?? $e['username'] ?? $e['privatePskName'] ?? null; + if ($entryName === $name) { + $applyUpdate($e); + $matched = true; + break; + } + } + unset($e); + } + + if (! $matched && $oldPassphrase !== null && $oldPassphrase !== '') { + foreach ($entries as &$e) { + $current = $e['x_passphrase'] ?? $e['password'] ?? $e['passphrase'] ?? null; + if ($current === $oldPassphrase) { + $applyUpdate($e); + $matched = true; + break; + } + } + unset($e); } - unset($e); if (! $matched) { - throw new \RuntimeException('Embedded PPSK not found by current passphrase.'); + throw new \RuntimeException( + 'Embedded PPSK not found' . + ($name !== null ? " by name \"{$name}\"" : '') . + ' or by current passphrase.' + ); } // UniFi REST expects the full WLAN object on PUT — send what we