From 655d8dd50cb4c11c61bbe8451ab836771025a7d2 Mon Sep 17 00:00:00 2001 From: jwed Date: Fri, 21 Aug 2026 13:07:16 -0400 Subject: [PATCH] Network presence no longer frees a device slot --- src/Http/Controllers/PortalController.php | 68 ++++++++++++----------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/Http/Controllers/PortalController.php b/src/Http/Controllers/PortalController.php index 3501c13..e1d5d3d 100644 --- a/src/Http/Controllers/PortalController.php +++ b/src/Http/Controllers/PortalController.php @@ -164,28 +164,30 @@ class PortalController extends Controller return Inertia::render('Unifi/PortalDenied', ['reason' => 'No WiFi access configured for your account.']); } - // Check device limit - $activeSessions = PortalSession::where('user_id', $user->id) - ->where('is_active', true) - ->get(); + /* + * Presence is NOT the question — a live authorization is. + * + * This used to walk the user's sessions and mark any device UniFi did not + * currently see as stale, freeing its slot. But UniFi holds an + * authorization for its whole window regardless of whether the device is + * in the building, so a phone that went home had its slot freed while + * still being authorized: the owner then authorized a second device and + * one of the two was an orphan nothing tracked. The longer + * session_minutes is — and it is now meant to be a term — the more of + * those accumulate. + * + * Slots are now freed by eviction, by expiry, or by ReconcilePortal. + */ + $live = PortalSession::live()->where('user_id', $user->id)->get(); - foreach ($activeSessions as $session) { - if ($session->mac_address === $clientMac) { - // Same device reconnecting — refresh - $session->update(['is_active' => false]); - continue; - } - // Check if other device is still actually connected - if (! $unifi->isClientConnected($session->mac_address)) { - $session->update(['is_active' => false]); // stale session - } - } + // The same device coming back is a renewal, not a new device: it keeps + // its slot and its row, and simply gets a fresh window. + $existing = $live->firstWhere('mac_address', $clientMac); - $activeCount = PortalSession::where('user_id', $user->id)->where('is_active', true)->count(); - if ($activeCount >= $mapping->max_devices) { + if (! $existing && $live->count() >= $mapping->max_devices) { return Inertia::render('Unifi/PortalDenied', [ - 'reason' => "You already have {$activeCount} device(s) connected (limit: {$mapping->max_devices}).", - 'sessions' => $activeSessions->map(fn ($s) => [ + 'reason' => "You already have {$live->count()} device(s) connected (limit: {$mapping->max_devices}).", + 'sessions' => $live->map(fn ($s) => [ 'id' => $s->id, 'mac' => $s->mac_address, 'hostname' => $s->device_hostname, @@ -200,20 +202,20 @@ class PortalController extends Controller $minutes = $mapping->session_minutes; $unifi->authorizeGuest($clientMac, $minutes); - // Record the session - PortalSession::create([ - 'user_id' => $user->id, - 'mac_address' => $clientMac, - 'device_hostname' => $request->input('hostname'), - 'device_os' => $request->input('os'), - 'device_type' => $request->input('dev_cat'), - 'ssid' => $ssid, - 'vlan_id' => $mapping->vlan_id, - 'ap_mac' => $apMac, - 'is_active' => true, - 'authorized_at' => now(), - 'expires_at' => now()->addMinutes($minutes), - ]); + PortalSession::updateOrCreate( + ['user_id' => $user->id, 'mac_address' => $clientMac], + [ + 'device_hostname' => $request->input('hostname'), + 'device_os' => $request->input('os'), + 'device_type' => $request->input('dev_cat'), + 'ssid' => $ssid, + 'vlan_id' => $mapping->vlan_id, + 'ap_mac' => $apMac, + 'is_active' => true, + 'authorized_at' => now(), + 'expires_at' => now()->addMinutes($minutes), + ] + ); // Redirect to the original URL the user was trying to reach. //