fix(rotate): record PPSK rotation password + sync banded-SSID siblings
Three bugs reported from prod after a PPSK rotation: 1. unifi.password_rotation.last_password was only saved after a whole-SSID rotation. PPSK-only setups (the typical guest-WiFi case) ran a successful rotation but the setting stayed empty, so the Settings → Tasks UI never showed the current password and the /api/unifi/wifi/current-password endpoint returned 404 "no rotated password recorded yet". The PPSK loop now writes last_password on every successful PPSK rotation. 2. When an SSID is "banded" (band-steering disabled), UniFi splits it into one wlanconf per band — 2.4GHz and 5GHz each get their own _id and their own embedded PPSK array. Rotating the PPSK on one band left the other band with the old password. New UnifiApiClient::getWlanSiblings($wlanId) finds all wlanconfs that share an SSID name; both rotation and the manual modal edit now call updateEmbeddedPpsk on each sibling and update the matching UnifiPpsk DB rows. 3. The manual WiFi modal edit had the same band-blindness as #2 — editing the GUEST PPSK on the 2.4GHz half left the 5GHz half stale. WifiController::ppskUpdate now walks siblings the same way. v1.8.1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -84,12 +84,44 @@ class RotatePasswords extends Command
|
||||
// Synthetic ID is derived from the new passphrase, so update it too.
|
||||
$unifi->updateEmbeddedPpsk($ppsk->wlan_id, $ppsk->x_passphrase, $newPass);
|
||||
$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.
|
||||
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);
|
||||
if ($sibling) {
|
||||
$sibling->update([
|
||||
'x_passphrase' => $newPass,
|
||||
'unifi_id' => 'emb_' . substr(hash('sha256', $siblingWlanId . ':' . $newPass), 0, 32),
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("Sibling rotate failed for wlan {$siblingWlanId}: {$e->getMessage()}");
|
||||
$failedPpsks[] = ['name' => $ppsk->name . ' (sibling wlan ' . $siblingWlanId . ')', 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$unifi->updatePpsk($ppsk->unifi_id, ['x_passphrase' => $newPass]);
|
||||
$ppsk->update(['x_passphrase' => $newPass]);
|
||||
}
|
||||
$rotatedPpsks[] = $ppsk->name;
|
||||
|
||||
// Save the active password every time a rotation
|
||||
// succeeds — covers PPSK-only rotation setups where
|
||||
// there's no whole-SSID rotation. Last successful
|
||||
// password wins if multiple PPSKs rotate in one run.
|
||||
Setting::set('unifi.password_rotation.last_password', $newPass);
|
||||
Setting::set('unifi.password_rotation.last_rotated_at', now()->toIso8601String());
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("Failed to rotate PPSK \"{$ppsk->name}\": {$e->getMessage()}");
|
||||
$failedPpsks[] = ['name' => $ppsk->name, 'error' => $e->getMessage()];
|
||||
|
||||
Reference in New Issue
Block a user