feat: password rotation, PPSK management, VLAN/AP groups
- Add password rotation: RotatePasswords console command + migration + service updates - Add PPSK management: UnifiPpsk model, migration, SyncPpskSchedules console - Add VLAN groups and AP groups: VlanGroupController, ApGroupController, model, migration - Add RebootAllAps console command - Add in_alert column to device states - Wire new features through service provider, routes, and existing controllers/services Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
97
src/Console/RotatePasswords.php
Normal file
97
src/Console/RotatePasswords.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Dashboard\Unifi\Console;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Dashboard\Unifi\Models\UnifiPpsk;
|
||||
use Dashboard\Unifi\Services\UnifiApiClient;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class RotatePasswords extends Command
|
||||
{
|
||||
protected $signature = 'unifi:rotate-passwords {--force : Run regardless of schedule}';
|
||||
protected $description = 'Rotate WiFi passwords for SSIDs configured with a wordlist schedule';
|
||||
|
||||
public function handle(UnifiApiClient $unifi): int
|
||||
{
|
||||
if (! Setting::get('unifi.password_rotation.enabled')) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$wlanIdsJson = Setting::get('unifi.password_rotation.wlan_ids', '[]');
|
||||
$wlanIds = json_decode($wlanIdsJson, true);
|
||||
|
||||
if (empty($wlanIds) || ! is_array($wlanIds)) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$wordlist = Setting::get('unifi.password_rotation.wordlist', '');
|
||||
$passwords = array_values(array_filter(array_map('trim', explode("\n", $wordlist))));
|
||||
|
||||
if (empty($passwords)) {
|
||||
$this->warn('Password rotation: no passwords in wordlist — skipped.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $this->option('force') && ! $this->isDue()) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$password = $passwords[array_rand($passwords)];
|
||||
$rotated = 0;
|
||||
|
||||
foreach ($wlanIds as $wlanId) {
|
||||
try {
|
||||
$unifi->updateWlan($wlanId, ['x_passphrase' => $password]);
|
||||
$rotated++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("Failed to rotate wlan {$wlanId}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
if ($rotated > 0) {
|
||||
Setting::set('unifi.password_rotation.last_rotated_at', now()->toIso8601String());
|
||||
$this->info("Rotated password for {$rotated} SSID(s).");
|
||||
}
|
||||
|
||||
// ── Rotate PPSK passwords ────────────────────────────────────────────
|
||||
$rotatedPpsks = 0;
|
||||
foreach (UnifiPpsk::where('rotate_password', true)->where('state', 'active')->whereNotNull('unifi_id')->get() as $ppsk) {
|
||||
// Each PPSK gets its own independently-chosen password from the wordlist
|
||||
$newPass = $passwords[array_rand($passwords)];
|
||||
try {
|
||||
$unifi->updatePpsk($ppsk->unifi_id, ['x_passphrase' => $newPass]);
|
||||
$ppsk->update(['x_passphrase' => $newPass]);
|
||||
$rotatedPpsks++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("Failed to rotate PPSK \"{$ppsk->name}\": {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
if ($rotatedPpsks > 0) {
|
||||
$this->info("Rotated password for {$rotatedPpsks} PPSK(s).");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function isDue(): bool
|
||||
{
|
||||
$frequency = Setting::get('unifi.password_rotation.frequency', 'weekly');
|
||||
$hour = (int) Setting::get('unifi.password_rotation.hour', 2);
|
||||
$minute = (int) Setting::get('unifi.password_rotation.minute', 0);
|
||||
$dow = (int) Setting::get('unifi.password_rotation.day_of_week', 0);
|
||||
$tz = Setting::get('unifi.timezone', 'UTC');
|
||||
$now = now($tz);
|
||||
|
||||
if ($now->hour !== $hour || $now->minute !== $minute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($frequency === 'weekly' && $now->dayOfWeek !== $dow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user