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:
2026-05-19 17:54:24 -04:00
parent ce3217d8f4
commit 0802ef35f3
22 changed files with 1771 additions and 305 deletions

View File

@@ -13,52 +13,79 @@ class UnifiSettingsController extends Controller
public function edit()
{
return Inertia::render('Unifi/Settings', [
'controllerUrl' => Setting::get('unifi.controller_url', ''),
'username' => Setting::get('unifi.username', ''),
'hasPassword' => (bool) Setting::get('unifi.password'),
'hasApiKey' => (bool) Setting::get('unifi.api_key'),
'site' => Setting::get('unifi.site', 'default'),
'pollInterval' => (int) Setting::get('unifi.poll_interval', 30),
'cacheTtl' => (int) Setting::get('unifi.cache_ttl', 30),
'retentionDays' => (int) Setting::get('unifi.retention_days', 30),
'controllerUrl' => Setting::get('unifi.controller_url', ''),
'hasApiKey' => (bool) Setting::get('unifi.api_key'),
'site' => Setting::get('unifi.site', 'default'),
'pollInterval' => (int) Setting::get('unifi.poll_interval', 30),
'cacheTtl' => (int) Setting::get('unifi.cache_ttl', 30),
'retentionDays' => (int) Setting::get('unifi.retention_days', 30),
'timezone' => Setting::get('unifi.timezone', 'UTC'),
'autoRebootEnabled' => (bool) Setting::get('unifi.auto_reboot.enabled', false),
'autoRebootFrequency' => Setting::get('unifi.auto_reboot.frequency', 'daily'),
'autoRebootDow' => (int) Setting::get('unifi.auto_reboot.day_of_week', 0),
'autoRebootHour' => (int) Setting::get('unifi.auto_reboot.hour', 2),
'autoRebootMinute' => (int) Setting::get('unifi.auto_reboot.minute', 0),
'rotationEnabled' => (bool) Setting::get('unifi.password_rotation.enabled', false),
'rotationFrequency' => Setting::get('unifi.password_rotation.frequency', 'weekly'),
'rotationDow' => (int) Setting::get('unifi.password_rotation.day_of_week', 0),
'rotationHour' => (int) Setting::get('unifi.password_rotation.hour', 2),
'rotationMinute' => (int) Setting::get('unifi.password_rotation.minute', 0),
'rotationWordlist' => Setting::get('unifi.password_rotation.wordlist', ''),
'rotationLastRotatedAt' => Setting::get('unifi.password_rotation.last_rotated_at', null),
'ppskSchedulingEnabled' => (bool) Setting::get('unifi.ppsk_scheduling.enabled', false),
]);
}
public function update(Request $request)
{
$request->validate([
'controller_url' => 'required|url|max:500',
'username' => 'nullable|string|max:255',
'password' => 'nullable|string|max:255',
'api_key' => 'nullable|string|max:500',
'site' => 'required|string|max:100',
'poll_interval' => 'nullable|integer|min:5|max:300',
'cache_ttl' => 'nullable|integer|min:5|max:300',
'retention_days' => 'nullable|integer|min:1|max:365',
'controller_url' => 'required|url|max:500',
'api_key' => 'nullable|string|max:500',
'site' => 'required|string|max:100',
'poll_interval' => 'nullable|integer|min:5|max:300',
'cache_ttl' => 'nullable|integer|min:5|max:300',
'retention_days' => 'nullable|integer|min:1|max:365',
'timezone' => 'nullable|string|timezone',
'auto_reboot_enabled' => 'boolean',
'auto_reboot_frequency' => 'in:daily,weekly',
'auto_reboot_dow' => 'nullable|integer|min:0|max:6',
'auto_reboot_hour' => 'nullable|integer|min:0|max:23',
'auto_reboot_minute' => 'nullable|integer|min:0|max:59',
'rotation_enabled' => 'boolean',
'rotation_frequency' => 'in:daily,weekly',
'rotation_dow' => 'nullable|integer|min:0|max:6',
'rotation_hour' => 'nullable|integer|min:0|max:23',
'rotation_minute' => 'nullable|integer|min:0|max:59',
'rotation_wordlist' => 'nullable|string|max:20000',
'ppsk_scheduling_enabled' => 'boolean',
]);
Setting::set('unifi.controller_url', rtrim($request->controller_url, '/'));
Setting::set('unifi.site', $request->site);
// Save the chosen auth method and clear the other
Setting::set('unifi.username', $request->username ?? '');
if ($request->password && $request->password !== '••••••••') {
Setting::set('unifi.password', $request->password);
} elseif (! $request->username) {
Setting::set('unifi.password', ''); // clear password when switching to API key mode
}
if ($request->api_key && $request->api_key !== '••••••••') {
Setting::set('unifi.api_key', $request->api_key);
} elseif ($request->username) {
Setting::set('unifi.api_key', ''); // clear API key when switching to local account mode
}
if ($request->has('poll_interval')) Setting::set('unifi.poll_interval', $request->poll_interval ?? 30);
if ($request->has('cache_ttl')) Setting::set('unifi.cache_ttl', $request->cache_ttl ?? 30);
if ($request->has('cache_ttl')) Setting::set('unifi.cache_ttl', $request->cache_ttl ?? 30);
if ($request->has('retention_days')) Setting::set('unifi.retention_days', $request->retention_days ?? 30);
if ($request->has('timezone')) Setting::set('unifi.timezone', $request->timezone ?? 'UTC');
Setting::set('unifi.auto_reboot.enabled', $request->boolean('auto_reboot_enabled') ? '1' : '');
Setting::set('unifi.auto_reboot.frequency', $request->input('auto_reboot_frequency', 'daily'));
Setting::set('unifi.auto_reboot.day_of_week',$request->input('auto_reboot_dow', 0));
Setting::set('unifi.auto_reboot.hour', $request->input('auto_reboot_hour', 2));
Setting::set('unifi.auto_reboot.minute', $request->input('auto_reboot_minute', 0));
Setting::set('unifi.password_rotation.enabled', $request->boolean('rotation_enabled') ? '1' : '');
Setting::set('unifi.password_rotation.frequency', $request->input('rotation_frequency', 'weekly'));
Setting::set('unifi.password_rotation.day_of_week', $request->input('rotation_dow', 0));
Setting::set('unifi.password_rotation.hour', $request->input('rotation_hour', 2));
Setting::set('unifi.password_rotation.minute', $request->input('rotation_minute', 0));
Setting::set('unifi.password_rotation.wordlist', $request->input('rotation_wordlist', ''));
Setting::set('unifi.ppsk_scheduling.enabled', $request->boolean('ppsk_scheduling_enabled') ? '1' : '');
// Clear cached sessions so new credentials take effect
\Illuminate\Support\Facades\Cache::forget('unifi:session:' . md5(rtrim($request->controller_url, '/') . $request->username));
\Illuminate\Support\Facades\Cache::forget('unifi:api_prefix:' . md5(rtrim($request->controller_url, '/')));
return back()->with('success', 'UniFi settings saved.');
@@ -103,12 +130,10 @@ class UnifiSettingsController extends Controller
$hint = "Tried URL: {$url}. ";
if (str_contains($url, 'unifi.ui.com')) {
$hint .= "Use your console's direct URL (*.id.ui.direct or local IP) instead of unifi.ui.com.";
} elseif (! $user && ! $key) {
$hint .= "Enter either a local account username/password or an API key.";
} elseif (! $key) {
$hint .= "Enter an API key above.";
} else {
$hint .= $user
? "Check that the local account credentials are correct."
: "The API key may be read-only. Try using a local admin account instead.";
$hint .= "Check that the API key is correct and the controller URL is reachable.";
}
return response()->json(['ok' => false, 'error' => $e->getMessage(), 'hint' => $hint], 422);