Files
central-unifi/src/Console/RotatePasswords.php
jwed f7672771e0 refactor: read timezone from shell-level site_timezone
Drops unifi.timezone from the settings form (now lives in
Admin → Settings on the shell). Schedulers (PPSK sync, password
rotation) now read \App\Support\Timezone::current() — same fallback
chain as the rest of the platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 15:26:50 -04:00

98 lines
3.5 KiB
PHP

<?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 = \App\Support\Timezone::current();
$now = now($tz);
if ($now->hour !== $hour || $now->minute !== $minute) {
return false;
}
if ($frequency === 'weekly' && $now->dayOfWeek !== $dow) {
return false;
}
return true;
}
}