From 8f51be8515292a63b778b4d6213001c62bcae305 Mon Sep 17 00:00:00 2001 From: jwed Date: Sun, 24 May 2026 17:53:48 -0400 Subject: [PATCH] fix(rotate): don't skip when only PPSKs are flagged; move webhooks under /settings * Password rotation was short-circuiting any run that had no whole-SSID wlan_ids configured, even if there were PPSKs with rotate_password=true in the database. The PPSK rotation block lived after the early-return, so per-PPSK rotation never fired. Now we only skip when there's nothing at all to rotate (neither wlan_ids nor PPSK opt-ins). * Webhook routes moved from /app/network/webhooks to /app/network/settings/webhooks so the URL reflects that this is a settings tab. Route names unchanged. v1.5.3. Co-Authored-By: Claude Opus 4.7 (1M context) --- composer.json | 2 +- src/Console/RotatePasswords.php | 13 ++++++++++--- src/routes/unifi.php | 12 ++++++------ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 08550f1..93c5df9 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.5.2", + "version": "1.5.3", "type": "library", "license": "MIT", "autoload": { diff --git a/src/Console/RotatePasswords.php b/src/Console/RotatePasswords.php index a168d50..f3f9c14 100644 --- a/src/Console/RotatePasswords.php +++ b/src/Console/RotatePasswords.php @@ -32,9 +32,16 @@ class RotatePasswords extends Command $run = UnifiCronRun::record('rotate-passwords', $triggeredBy, null, function () use ($unifi, $force) { $wlanIdsJson = Setting::get('unifi.password_rotation.wlan_ids', '[]'); $wlanIds = json_decode($wlanIdsJson, true); + if (! is_array($wlanIds)) $wlanIds = []; - if (empty($wlanIds) || ! is_array($wlanIds)) { - return ['status' => 'skipped', 'reason' => 'no SSIDs configured for rotation']; + $ppskQuery = UnifiPpsk::where('rotate_password', true) + ->where('state', 'active') + ->whereNotNull('unifi_id'); + + // Skip only if there's nothing at all to rotate — neither + // whole-SSID rotation targets nor per-PPSK rotation opt-ins. + if (empty($wlanIds) && ! $ppskQuery->exists()) { + return ['status' => 'skipped', 'reason' => 'no SSIDs or PPSKs configured for rotation']; } $wordlist = Setting::get('unifi.password_rotation.wordlist', ''); @@ -66,7 +73,7 @@ class RotatePasswords extends Command $rotatedPpsks = []; $failedPpsks = []; - foreach (UnifiPpsk::where('rotate_password', true)->where('state', 'active')->whereNotNull('unifi_id')->get() as $ppsk) { + foreach ($ppskQuery->get() as $ppsk) { $newPass = $passwords[array_rand($passwords)]; try { $unifi->updatePpsk($ppsk->unifi_id, ['x_passphrase' => $newPass]); diff --git a/src/routes/unifi.php b/src/routes/unifi.php index 2829fca..8361fe2 100644 --- a/src/routes/unifi.php +++ b/src/routes/unifi.php @@ -83,12 +83,12 @@ Route::middleware(['web', 'auth', 'app.access:unifi']) // Cron logs — read-only history of scheduled-task runs. Route::get('/settings/cron-logs', [UnifiCronLogsController::class, 'index'])->name('settings.cron-logs.index'); - // Webhooks - Route::get('/webhooks', [WebhookController::class, 'index']) ->name('webhooks.index'); - Route::post('/webhooks', [WebhookController::class, 'store']) ->name('webhooks.store'); - Route::put('/webhooks/{webhook}', [WebhookController::class, 'update']) ->name('webhooks.update'); - Route::delete('/webhooks/{webhook}', [WebhookController::class, 'destroy'])->name('webhooks.destroy'); - Route::post('/webhooks/{webhook}/test', [WebhookController::class, 'test']) ->name('webhooks.test'); + // Webhooks — lives under /settings/* so it reads as a settings tab. + Route::get('/settings/webhooks', [WebhookController::class, 'index']) ->name('webhooks.index'); + Route::post('/settings/webhooks', [WebhookController::class, 'store']) ->name('webhooks.store'); + Route::put('/settings/webhooks/{webhook}', [WebhookController::class, 'update']) ->name('webhooks.update'); + Route::delete('/settings/webhooks/{webhook}', [WebhookController::class, 'destroy'])->name('webhooks.destroy'); + Route::post('/settings/webhooks/{webhook}/test', [WebhookController::class, 'test']) ->name('webhooks.test'); }); });