Gate the guest Wi-Fi card on its own key, and gate its poll endpoint properly

The home card used to be grant-gated by the shell's card table, which left the
poll endpoint behind `auth` alone — and that endpoint returns a LIVE CREDENTIAL:
the guest network's current rotating password and its join QR code. "Any logged-in
user" was never an adequate gate for that; the card table was doing the work, and
the card table is gone.

New key rather than reuse: unifi.guest_wifi exists precisely so front-desk staff
can be given the guest password WITHOUT any other network access. Reusing
unifi.settings would have meant handing out controller configuration to get a
Wi-Fi password read.

Authorised inside the controller rather than by route middleware, so the endpoint
stays outside the unifi page-permission groups — it is a home-page widget, not a
unifi page.

Nobody holds the new key until it is granted, so the card is invisible until then.
That is the intended direction: capability is assigned deliberately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDA3fQbsa3gaHCFoV1Q8Gg
This commit is contained in:
2026-07-30 12:15:05 -04:00
parent c93f73ce02
commit 186c1eaf2b
4 changed files with 47 additions and 18 deletions

View File

@@ -57,29 +57,41 @@ class GuestCardController extends Controller
* unchecked, so any signed-in account — every student and parent included —
* could fetch the live guest Wi-Fi password by requesting the URL directly.
*
* The endpoint now enforces what the comment always claimed: you may poll it iff
* you may see the card. Super admins pass, as everywhere else. Kept as the card
* gate rather than a permission so the widget stays grantable to non-unifi staff,
* which is the point of a home-page card.
* The endpoint enforces what the comment always claimed: you may poll it iff
* you may see the card. Still asked as the CARD gate — via the registry, which
* now answers from the card's permission (unifi.guest_wifi) instead of the
* retired dashboard_card_grants table. Asking the registry rather than naming
* the key here is deliberate: the endpoint and the home page cannot drift.
* Super admins pass, as everywhere else (keysFor handles that).
*/
public function data(Request $request)
{
$user = $request->user();
abort_unless($user, 403);
if (! $user->is_super_admin) {
$allowed = \App\Models\DashboardCardGrant::accessibleCardKeysFor($user)
->contains(self::CARD_KEY);
abort_unless($allowed, 403);
}
abort_unless(
app(\App\Support\DashboardCardRegistry::class)->visibleTo($user, self::CARD_KEY),
403,
);
return response()->json(self::payload() ?? ['title' => null]);
}
/** Card registry resolver. */
/**
* Card registry resolver.
*
* Re-checks the permission the registry already gated on, the way bosch's and
* signinout's resolvers do. Redundant on the home-page path and deliberately
* kept: renderFor() renders whatever key list it is handed, and the payload
* here is a live credential — the one card where "the caller asked the right
* question" is not a good enough reason to hand it over.
*/
public static function resolve(User $user): ?array
{
if (! $user->can('unifi.guest_wifi')) {
return null;
}
return self::payload();
}