mergeConfigFrom(__DIR__ . '/../config/unifi.php', 'unifi'); $this->app->singleton(Services\UnifiApiClient::class, function ($app) { return new Services\UnifiApiClient(); }); } public function boot(): void { $this->loadRoutesFrom(__DIR__ . '/routes/unifi.php'); $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); // 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 // shell also folds these grants back into User::can()/allPermissions // so the route-level permission middleware passes for grantees. try { app(\App\Support\NavVisibilityRegistry::class)->register( 'unifi.', fn (\App\Models\User $user) => $this->visibleUnifiNavItemIdsFor($user), ); } catch (\Throwable) { // Shell may not have the registry yet (older shell version). // Sidebar will fall back to legacy permission filter. } // Per-page enforcement for unifi pages. Settings stays on its // permission:unifi.settings route middleware (the Access tab // itself lives there and must not be able to lock itself out). // The user-dependent check lives in middleware appended to the // matched route: RouteMatched fires before the session/auth // middleware run, so request->user() is null here and any check // at this point silently fails open. Event::listen(RouteMatched::class, function (RouteMatched $event) { $routeName = $event->route->getName(); if (! $routeName || ! str_starts_with($routeName, 'unifi.')) return; if (str_starts_with($routeName, 'unifi.settings')) return; $event->route->middleware(\Dashboard\Unifi\Http\Middleware\EnforceUnifiPageGrant::class); }); if ($this->app->runningInConsole()) { $this->commands([ Console\CheckWebhooks::class, Console\CaptureSnapshots::class, Console\CleanupSnapshots::class, Console\RebootAllAps::class, Console\RotatePasswords::class, Console\SyncPpskSchedules::class, ]); $this->publishes([ __DIR__ . '/../config/unifi.php' => config_path('unifi.php'), ], 'unifi-config'); } } /** Collect every unifi nav_item_id this user is allowed to see. */ protected function visibleUnifiNavItemIdsFor(\App\Models\User $user): \Illuminate\Support\Collection { $ids = collect(); // Pages whose required_permission the user already holds through // groups keep showing, independent of the access-grant matrix. $permittedIds = NavItem::where('route_name', 'like', 'unifi.%') ->whereIn('required_permission', static::realPermissionKeysFor($user)) ->pluck('id'); $ids = $ids->merge($permittedIds); if (\Illuminate\Support\Facades\Schema::hasTable('unifi_page_grants')) { $groupIds = $user->groups()->pluck('groups.id'); $roleIds = $user->roles()->pluck('roles.id'); $grantedIds = UnifiPageGrant::query() ->where('can_view', true) ->where(function ($q) use ($user, $groupIds, $roleIds) { $q->where(fn ($u) => $u->where('grantee_type', 'user')->where('grantee_id', $user->id)) ->orWhere(fn ($g) => $g->where('grantee_type', 'group')->whereIn('grantee_id', $groupIds)) ->orWhere(fn ($r) => $r->where('grantee_type', 'role')->whereIn('grantee_id', $roleIds)) // Default-allow row makes the page visible to everyone. ->orWhere('grantee_type', 'default'); }) ->pluck('nav_item_id'); $ids = $ids->merge($grantedIds); } return $ids->unique()->values(); } /** * Group + direct permissions only. Deliberately NOT allPermissions(): * the shell folds page-grant permissions back into allPermissions/can, * so using it here would turn a single granted page into every page * carrying the same permission. */ public static function realPermissionKeysFor(\App\Models\User $user): \Illuminate\Support\Collection { if ($user->is_super_admin) { return \App\Models\Permission::pluck('key'); } $direct = $user->directPermissions()->get(); return \App\Models\Permission::whereHas('groups', fn ($q) => $q->whereIn('group_id', $user->groups()->pluck('groups.id'))) ->pluck('key') ->merge($direct->where('granted', true)->pluck('permission_key')) ->diff($direct->where('granted', false)->pluck('permission_key')) ->unique() ->values(); } }