diff --git a/src/Http/Controllers/UnifiPagesAccessController.php b/src/Http/Controllers/UnifiPagesAccessController.php deleted file mode 100644 index 27b8128..0000000 --- a/src/Http/Controllers/UnifiPagesAccessController.php +++ /dev/null @@ -1,207 +0,0 @@ -first(); - if (! $app) { - return response()->json(['pages' => [], 'users' => [], 'groups' => [], 'roles' => []]); - } - - $pages = NavItem::where('app_id', $app->id) - ->where('is_folder', false) - ->whereNotNull('route_name') - ->orderBy('sort_order') - ->get(['id', 'label', 'route_name']); - - $grants = UnifiPageGrant::whereIn('nav_item_id', $pages->pluck('id')) - ->get() - ->groupBy('nav_item_id'); - - // Only return users that ALREADY have grants. The full users list - // can be enormous (thousands of rows); the operator adds more via - // the searchUsers endpoint as needed. - $grantedUserIds = $grants->flatten(1)->where('grantee_type', 'user')->pluck('grantee_id')->unique(); - $users = User::whereIn('id', $grantedUserIds)->orderBy('name')->get(['id', 'name', 'email']); - - // Groups: always include super-admin groups (locked-on across all - // pages) plus any group with at least one grant. Other groups are - // added via searchGroups. - $grantedGroupIds = $grants->flatten(1)->where('grantee_type', 'group')->pluck('grantee_id')->unique(); - $groups = Group::where(function ($q) use ($grantedGroupIds) { - $q->where('is_super', true) - ->orWhereIn('id', $grantedGroupIds); - })->orderBy('name')->get(['id', 'name', 'is_super']); - - // Roles: only ones with at least one grant — more added via searchRoles. - $grantedRoleIds = $grants->flatten(1)->where('grantee_type', 'role')->pluck('grantee_id')->unique(); - $roles = Role::whereIn('id', $grantedRoleIds)->orderBy('label')->get(['id', 'slug', 'label']); - - return response()->json([ - 'pages' => $pages->map(function ($p) use ($grants) { - $pageGrants = $grants->get($p->id, collect()); - $defaultRow = $pageGrants->firstWhere('grantee_type', 'default'); - return [ - 'id' => $p->id, - 'label' => $p->label, - 'route_name' => $p->route_name, - 'default_allow' => (bool) ($defaultRow?->can_view ?? false), - 'user_ids' => $pageGrants->where('grantee_type', 'user')->where('can_view', true)->pluck('grantee_id')->values()->all(), - 'group_ids' => $pageGrants->where('grantee_type', 'group')->where('can_view', true)->pluck('grantee_id')->values()->all(), - 'role_ids' => $pageGrants->where('grantee_type', 'role')->where('can_view', true)->pluck('grantee_id')->values()->all(), - ]; - })->values(), - 'users' => $users, - 'groups' => $groups, - 'roles' => $roles, - ]); - } - - /** - * Typeahead-style search for users to add to the access matrix. - * Returns up to 20 matches against name or email. Empty query returns - * an empty array — caller must enter at least 2 chars. - */ - public function searchUsers(Request $request) - { - $q = trim((string) $request->query('q', '')); - if (strlen($q) < 2) { - return response()->json(['users' => []]); - } - - $users = User::where(function ($w) use ($q) { - $w->where('name', 'like', '%' . $q . '%') - ->orWhere('email', 'like', '%' . $q . '%'); - }) - ->orderBy('name') - ->limit(20) - ->get(['id', 'name', 'email']); - - return response()->json(['users' => $users]); - } - - /** - * Typeahead-style search for groups to add to the access matrix. - * Excludes super-admin groups (they're already in the matrix and - * locked-on across every page). - */ - public function searchGroups(Request $request) - { - $q = trim((string) $request->query('q', '')); - if (strlen($q) < 2) { - return response()->json(['groups' => []]); - } - - $groups = Group::where('name', 'like', '%' . $q . '%') - ->where(function ($w) { $w->where('is_super', false)->orWhereNull('is_super'); }) - ->orderBy('name') - ->limit(20) - ->get(['id', 'name', 'is_super']); - - return response()->json(['groups' => $groups]); - } - - /** - * Typeahead-style search for roles to add to the access matrix. - * An empty query returns every role (the role list is small). - */ - public function searchRoles(Request $request) - { - $q = trim((string) $request->query('q', '')); - if (strlen($q) < 1) { - return response()->json(['roles' => Role::orderBy('label')->get(['id', 'slug', 'label'])]); - } - - $roles = Role::where('label', 'like', '%' . $q . '%') - ->orWhere('slug', 'like', '%' . $q . '%') - ->orderBy('label') - ->limit(20) - ->get(['id', 'slug', 'label']); - - return response()->json(['roles' => $roles]); - } - - public function update(Request $request, NavItem $navItem) - { - $app = DashboardApp::where('slug', 'unifi')->first(); - if (! $app || $navItem->app_id !== $app->id) { - return response()->json(['error' => 'Not a unifi page.'], 422); - } - - $data = $request->validate([ - 'default_allow' => 'boolean', - 'user_ids' => 'present|array', - 'user_ids.*' => 'integer|exists:users,id', - 'group_ids' => 'present|array', - 'group_ids.*' => 'integer|exists:groups,id', - 'role_ids' => 'array', - 'role_ids.*' => 'integer|exists:roles,id', - ]); - - $grantedBy = $request->user()?->id; - - DB::transaction(function () use ($navItem, $data, $grantedBy) { - // Upsert the default row (one per nav_item). firstOrCreate can't - // match on grantee_id=NULL reliably in MySQL, so look it up first. - $default = UnifiPageGrant::where('nav_item_id', $navItem->id) - ->where('grantee_type', 'default') - ->first(); - if ($default) { - $default->update(['can_view' => (bool) ($data['default_allow'] ?? false)]); - } else { - UnifiPageGrant::create([ - 'nav_item_id' => $navItem->id, - 'grantee_type' => 'default', - 'grantee_id' => null, - 'can_view' => (bool) ($data['default_allow'] ?? false), - 'granted_by_user_id' => $grantedBy, - ]); - } - - $this->syncGrantsOfType($navItem->id, 'user', $data['user_ids'] ?? [], $grantedBy); - $this->syncGrantsOfType($navItem->id, 'group', $data['group_ids'] ?? [], $grantedBy); - $this->syncGrantsOfType($navItem->id, 'role', $data['role_ids'] ?? [], $grantedBy); - }); - - return response()->json(['ok' => true]); - } - - private function syncGrantsOfType(int $navItemId, string $type, array $ids, ?int $actorId): void - { - UnifiPageGrant::where('nav_item_id', $navItemId) - ->where('grantee_type', $type) - ->whereNotIn('grantee_id', $ids ?: [0]) - ->delete(); - - foreach ($ids as $id) { - UnifiPageGrant::updateOrCreate( - [ - 'nav_item_id' => $navItemId, - 'grantee_type' => $type, - 'grantee_id' => $id, - ], - [ - 'can_view' => true, - 'granted_by_user_id' => $actorId, - ], - ); - } - } -} diff --git a/src/Models/UnifiPageGrant.php b/src/Models/UnifiPageGrant.php deleted file mode 100644 index d911ba5..0000000 --- a/src/Models/UnifiPageGrant.php +++ /dev/null @@ -1,65 +0,0 @@ - 'boolean', - ]; - - public function navItem(): BelongsTo - { - return $this->belongsTo(NavItem::class); - } - - public function grantedBy(): BelongsTo - { - return $this->belongsTo(User::class, 'granted_by_user_id'); - } - - /** - * Grant check with a per-page default fallback. Passes if the user - * matches an explicit user/group/role grant; otherwise falls back - * to the page's `default` row (deny by default — no row or - * can_view=false denies). Permission-based access is checked - * separately by the caller — grants extend, never revoke. - */ - public static function userCanAccess(User $user, NavItem $navItem): bool - { - if ($user->is_super_admin) return true; - - $groupIds = $user->groups()->pluck('groups.id'); - $roleIds = $user->roles()->pluck('roles.id'); - - $hasExplicit = static::where('nav_item_id', $navItem->id) - ->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)); - }) - ->exists(); - - if ($hasExplicit) return true; - - return (bool) static::where('nav_item_id', $navItem->id) - ->where('grantee_type', 'default') - ->value('can_view'); - } -} diff --git a/src/UnifiServiceProvider.php b/src/UnifiServiceProvider.php index 80ef368..019b82f 100644 --- a/src/UnifiServiceProvider.php +++ b/src/UnifiServiceProvider.php @@ -3,7 +3,6 @@ namespace Dashboard\Unifi; use App\Models\NavItem; -use Dashboard\Unifi\Models\UnifiPageGrant; use Illuminate\Support\ServiceProvider; class UnifiServiceProvider extends ServiceProvider @@ -58,38 +57,6 @@ class UnifiServiceProvider extends ServiceProvider } } - /** 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, diff --git a/src/routes/unifi.php b/src/routes/unifi.php index 2f37a0f..7054c51 100644 --- a/src/routes/unifi.php +++ b/src/routes/unifi.php @@ -75,17 +75,6 @@ Route::middleware(['web', 'auth', 'app.access:unifi']) Route::post('/settings/test', [UnifiSettingsController::class, 'testConnection'])->name('settings.test'); Route::post('/settings/sites', [UnifiSettingsController::class, 'fetchSites']) ->name('settings.sites'); - // Page Access — super-admin only. Lists unifi pages and lets - // operators assign per-page user/group/role grants plus the - // per-page "everyone else" default row (deny by default). - Route::middleware('super.admin')->group(function () { - Route::get('/settings/pages-access', [UnifiPagesAccessController::class, 'index']) ->name('settings.pages-access.index'); - Route::get('/settings/pages-access/users/search', [UnifiPagesAccessController::class, 'searchUsers'])->name('settings.pages-access.users.search'); - Route::get('/settings/pages-access/groups/search', [UnifiPagesAccessController::class, 'searchGroups'])->name('settings.pages-access.groups.search'); - Route::get('/settings/pages-access/roles/search', [UnifiPagesAccessController::class, 'searchRoles'])->name('settings.pages-access.roles.search'); - Route::put('/settings/pages-access/{navItem}', [UnifiPagesAccessController::class, 'update']) ->name('settings.pages-access.update'); - }); - // Cron logs — read-only history of scheduled-task runs. Route::get('/settings/cron-logs', [UnifiCronLogsController::class, 'index'])->name('settings.cron-logs.index');