Guest Wi-Fi dashboard card (network + rotating password + join QR)
New home card 'unifi.guest_wifi': shows the guest SSID, the current rotated password (unifi.password_rotation.last_password), and a WPA join QR built from the WIFI: string. Polls app/unifi/guest-card/data (auth) every 60s so it updates the moment the rotation engine changes the password — the QR regenerates client-side to match. Self-hides until enabled with an SSID and a rotated password; grant-gated via Admin -> Settings -> Dashboard Cards. New settings (WiFi rotation section): enable + network name + card title. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dashboard/unifi",
|
||||
"description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform",
|
||||
"version": "1.13.1",
|
||||
"version": "1.13.2",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
|
||||
65
src/Http/Controllers/GuestCardController.php
Normal file
65
src/Http/Controllers/GuestCardController.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Dashboard\Unifi\Http\Controllers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
/**
|
||||
* The "Guest Wi-Fi" dashboard home card: network name, the current
|
||||
* rotating password, and a QR code that re-generates when the password
|
||||
* changes. The password is the rotation engine's last-applied value
|
||||
* (unifi.password_rotation.last_password), so the card tracks whatever
|
||||
* unifi:rotate-passwords sets. The card polls data() so it updates
|
||||
* without a page reload.
|
||||
*/
|
||||
class GuestCardController extends Controller
|
||||
{
|
||||
/** Payload shared by the card resolver and the poll endpoint. */
|
||||
public static function payload(): ?array
|
||||
{
|
||||
if (! Setting::get('unifi.guest_card.enabled')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$password = Setting::get('unifi.password_rotation.last_password');
|
||||
$ssid = trim((string) Setting::get('unifi.guest_card.ssid', ''));
|
||||
if (! $password || $ssid === '') {
|
||||
return null; // nothing meaningful to show yet
|
||||
}
|
||||
|
||||
$freq = Setting::get('unifi.password_rotation.frequency', 'weekly');
|
||||
|
||||
return [
|
||||
'title' => trim((string) Setting::get('unifi.guest_card.title', '')) ?: 'Guest Wi-Fi',
|
||||
'ssid' => $ssid,
|
||||
'password' => $password,
|
||||
'rotated_at' => Setting::get('unifi.password_rotation.last_rotated_at'),
|
||||
// WPA/WPA2 join string a phone camera understands. Special
|
||||
// chars in SSID/password must be backslash-escaped per the
|
||||
// Wi-Fi QR spec.
|
||||
'qr' => self::wifiQrString($ssid, $password),
|
||||
'frequency' => in_array($freq, ['daily', 'weekly'], true) ? $freq : 'weekly',
|
||||
];
|
||||
}
|
||||
|
||||
/** Live poll for the card (any logged-in user who can see the card). */
|
||||
public function data()
|
||||
{
|
||||
return response()->json(self::payload() ?? ['title' => null]);
|
||||
}
|
||||
|
||||
/** Card registry resolver. */
|
||||
public static function resolve(User $user): ?array
|
||||
{
|
||||
return self::payload();
|
||||
}
|
||||
|
||||
private static function wifiQrString(string $ssid, string $password): string
|
||||
{
|
||||
$esc = fn (string $v) => preg_replace('/([\\\\;,:"])/', '\\\\$1', $v);
|
||||
|
||||
return sprintf('WIFI:T:WPA;S:%s;P:%s;;', $esc($ssid), $esc($password));
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,9 @@ class UnifiSettingsController extends Controller
|
||||
'ppskSchedulingEnabled' => (bool) Setting::get('unifi.ppsk_scheduling.enabled', false),
|
||||
'apiEnabled' => (bool) Setting::get('unifi.api.enabled', false),
|
||||
'apiToken' => Setting::get('unifi.api_token', null),
|
||||
'guestCardEnabled' => (bool) Setting::get('unifi.guest_card.enabled', false),
|
||||
'guestCardSsid' => Setting::get('unifi.guest_card.ssid', ''),
|
||||
'guestCardTitle' => Setting::get('unifi.guest_card.title', ''),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -74,6 +77,9 @@ class UnifiSettingsController extends Controller
|
||||
'rotation_wordlist' => 'nullable|string|max:20000',
|
||||
'ppsk_scheduling_enabled' => 'boolean',
|
||||
'api_enabled' => 'boolean',
|
||||
'guest_card_enabled' => 'boolean',
|
||||
'guest_card_ssid' => 'nullable|string|max:100',
|
||||
'guest_card_title' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
Setting::set('unifi.controller_url', rtrim($request->controller_url, '/'));
|
||||
@@ -101,6 +107,9 @@ class UnifiSettingsController extends Controller
|
||||
Setting::set('unifi.password_rotation.wordlist', $request->input('rotation_wordlist', ''));
|
||||
Setting::set('unifi.ppsk_scheduling.enabled', $request->boolean('ppsk_scheduling_enabled') ? '1' : '');
|
||||
Setting::set('unifi.api.enabled', $request->boolean('api_enabled') ? '1' : '');
|
||||
Setting::set('unifi.guest_card.enabled', $request->boolean('guest_card_enabled') ? '1' : '');
|
||||
Setting::set('unifi.guest_card.ssid', trim((string) $request->input('guest_card_ssid', '')));
|
||||
Setting::set('unifi.guest_card.title', trim((string) $request->input('guest_card_title', '')));
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget('unifi:api_prefix:' . md5(rtrim($request->controller_url, '/')));
|
||||
|
||||
|
||||
@@ -24,6 +24,22 @@ class UnifiServiceProvider extends ServiceProvider
|
||||
$this->loadRoutesFrom(__DIR__ . '/routes/unifi.php');
|
||||
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
|
||||
|
||||
// "/" home card: guest Wi-Fi network + current rotating password +
|
||||
// QR (re-renders when the password changes). Grant-gated via
|
||||
// Admin → Settings → Dashboard Cards; self-hides until enabled and
|
||||
// a password has been rotated.
|
||||
try {
|
||||
app(\App\Support\DashboardCardRegistry::class)->register(
|
||||
'unifi.guest_wifi',
|
||||
'Guest Wi-Fi',
|
||||
'UnifiGuestWifi',
|
||||
fn (\App\Models\User $user) => \Dashboard\Unifi\Http\Controllers\GuestCardController::resolve($user),
|
||||
'The guest network name, current password, and a join QR code — updates automatically when the password rotates.',
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
// Older shell without the card registry — skip.
|
||||
}
|
||||
|
||||
// Tell the shell's nav sidebar which unifi nav items the user
|
||||
// can see: pages whose required_permission the user holds through
|
||||
// groups, plus pages granted via the Settings → Access tab. The
|
||||
|
||||
@@ -103,6 +103,13 @@ Route::middleware(['web', 'auth', 'app.access:unifi'])
|
||||
});
|
||||
});
|
||||
|
||||
// ── Guest Wi-Fi home-card poll (any logged-in user; card visibility is
|
||||
// grant-gated by the shell). Exempt from the unifi page-grant middleware
|
||||
// because it's a home-page widget, not a unifi page. ──
|
||||
Route::middleware(['web', 'auth'])
|
||||
->get('/app/unifi/guest-card/data', [\Dashboard\Unifi\Http\Controllers\GuestCardController::class, 'data'])
|
||||
->name('unifi.guest-card.data');
|
||||
|
||||
// ── Public API (token-protected) ──────────────────────────────────────────
|
||||
// External integrations (signage, kiosks) hit these without session auth.
|
||||
Route::prefix('api/unifi')->name('unifi.api.')->group(function () {
|
||||
|
||||
Reference in New Issue
Block a user