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",
|
"name": "dashboard/unifi",
|
||||||
"description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform",
|
"description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform",
|
||||||
"version": "1.13.1",
|
"version": "1.13.2",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"autoload": {
|
"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),
|
'ppskSchedulingEnabled' => (bool) Setting::get('unifi.ppsk_scheduling.enabled', false),
|
||||||
'apiEnabled' => (bool) Setting::get('unifi.api.enabled', false),
|
'apiEnabled' => (bool) Setting::get('unifi.api.enabled', false),
|
||||||
'apiToken' => Setting::get('unifi.api_token', null),
|
'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',
|
'rotation_wordlist' => 'nullable|string|max:20000',
|
||||||
'ppsk_scheduling_enabled' => 'boolean',
|
'ppsk_scheduling_enabled' => 'boolean',
|
||||||
'api_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, '/'));
|
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.password_rotation.wordlist', $request->input('rotation_wordlist', ''));
|
||||||
Setting::set('unifi.ppsk_scheduling.enabled', $request->boolean('ppsk_scheduling_enabled') ? '1' : '');
|
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.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, '/')));
|
\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->loadRoutesFrom(__DIR__ . '/routes/unifi.php');
|
||||||
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
|
$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
|
// Tell the shell's nav sidebar which unifi nav items the user
|
||||||
// can see: pages whose required_permission the user holds through
|
// can see: pages whose required_permission the user holds through
|
||||||
// groups, plus pages granted via the Settings → Access tab. The
|
// 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) ──────────────────────────────────────────
|
// ── Public API (token-protected) ──────────────────────────────────────────
|
||||||
// External integrations (signage, kiosks) hit these without session auth.
|
// External integrations (signage, kiosks) hit these without session auth.
|
||||||
Route::prefix('api/unifi')->name('unifi.api.')->group(function () {
|
Route::prefix('api/unifi')->name('unifi.api.')->group(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user