From fcc58ca08a29561be07ae132cced2d8d87ea47b7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:47:35 +0100 Subject: [PATCH] fix(auth): enforce dashboard authorization and improve team deletion Add authorization gates to Project and Server creation buttons in the dashboard to prevent non-admin users from accessing resource creation. Improve team deletion to clear cache before deletion and automatically switch to the user's next available team. - Hide create buttons from non-admin users in dashboard - Clear cache before team deletion to prevent stale session resolution - Switch user session to next available team when current team is deleted - Handle refreshSession when user has no remaining teams - Add tests for dashboard authorization enforcement and team deletion flow --- app/Livewire/Team/Index.php | 11 +- bootstrap/helpers/shared.php | 8 ++ resources/views/livewire/dashboard.blade.php | 117 +++++++++++-------- tests/Feature/DashboardAuthorizationTest.php | 97 +++++++++++++++ tests/Feature/TeamDeletionTest.php | 54 +++++++++ 5 files changed, 234 insertions(+), 53 deletions(-) create mode 100644 tests/Feature/DashboardAuthorizationTest.php create mode 100644 tests/Feature/TeamDeletionTest.php diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php index e5ceb2cc9..140d9f5cc 100644 --- a/app/Livewire/Team/Index.php +++ b/app/Livewire/Team/Index.php @@ -7,6 +7,7 @@ use App\Models\TeamInvitation; use App\Support\ValidationPatterns; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Livewire\Component; @@ -98,8 +99,6 @@ class Index extends Component try { $currentTeam = currentTeam(); $this->authorize('delete', $currentTeam); - $currentTeam->delete(); - $currentTeam->members->each(function ($user) use ($currentTeam) { if ($user->id === Auth::id()) { return; @@ -111,7 +110,13 @@ class Index extends Component } }); - refreshSession(); + // Clear stale cache before deleting so refreshSession doesn't resolve the deleted team + Cache::forget('user:'.Auth::id().':team:'.$currentTeam->id); + $currentTeam->delete(); + + // Switch to the user's next available team + $newTeam = Auth::user()->teams()->first(); + refreshSession($newTeam); return redirect()->route('team.index'); } catch (\Throwable $e) { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 4372ff955..8428b7586 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -186,6 +186,14 @@ function refreshSession(?Team $team = null): void $team = User::find(Auth::id())->teams->first(); } } + + if (! $team) { + session()->forget('currentTeam'); + Cache::forget('team:'.Auth::id()); + + return; + } + // Clear old cache key format for backwards compatibility Cache::forget('team:'.Auth::id()); // Use new cache key format that includes team ID diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index a58ca0a00..7d1e932d6 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -17,20 +17,23 @@

Projects

- @if ($projects->count() > 0) - - - - - - - @endif + @can('create', App\Models\Project::class) + @if ($projects->count() > 0) + + + + + + + @endif + @endcan
@if ($projects->count() > 0)
@@ -70,12 +73,15 @@ @else
No projects found.
-
- - - your first project or - go to the onboarding page. -
+ @can('create', App\Models\Project::class) +
+ + + your first project or + go to the onboarding page. +
+ @endcan
@endif
@@ -83,20 +89,23 @@

Servers

- @if ($servers->count() > 0 && $privateKeys->count() > 0) - - - - - - - @endif + @can('create', App\Models\Server::class) + @if ($servers->count() > 0 && $privateKeys->count() > 0) + + + + + + + @endif + @endcan
@if ($servers->count() > 0)
@@ -133,26 +142,34 @@ @if ($privateKeys->count() === 0)
No private keys found.
-
Before you can add your server, first - - a private key - or - go to the onboarding - page. -
+ @can('create', App\Models\Server::class) +
Before you can add your server, first + + a private key + or + go to the onboarding + page. +
+ @endcan
@else
No servers found.
-
- - - your first server - or - go to the onboarding - page. -
+ @can('create', App\Models\Server::class) +
+ + + your first server + or + go to the onboarding + page. +
+ @endcan
@endif @endif diff --git a/tests/Feature/DashboardAuthorizationTest.php b/tests/Feature/DashboardAuthorizationTest.php new file mode 100644 index 000000000..26a39af0a --- /dev/null +++ b/tests/Feature/DashboardAuthorizationTest.php @@ -0,0 +1,97 @@ +create(); + + $user = User::factory()->create(); + $user->teams()->attach($team, ['role' => $role]); + + return [$user, $team]; +} + +function createProjectForTeam(Team $team): void +{ + Project::create([ + 'uuid' => (string) Str::uuid(), + 'name' => 'Test Project', + 'team_id' => $team->id, + ]); +} + +function createServerWithKeyForTeam(Team $team): void +{ + $keyId = DB::table('private_keys')->insertGetId([ + 'uuid' => (string) Str::uuid(), + 'name' => 'Test Key', + 'private_key' => 'test-key', + 'team_id' => $team->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + Server::factory()->create([ + 'team_id' => $team->id, + 'private_key_id' => $keyId, + ]); +} + +test('admin sees add project button on dashboard', function () { + [$user, $team] = setupDashboardUser('admin'); + + $this->actingAs($user); + session(['currentTeam' => $team]); + + createProjectForTeam($team); + + Livewire::test(Dashboard::class) + ->assertSee('New Project'); +}); + +test('member does not see add project button on dashboard', function () { + [$user, $team] = setupDashboardUser('member'); + + $this->actingAs($user); + session(['currentTeam' => $team]); + + createProjectForTeam($team); + + Livewire::test(Dashboard::class) + ->assertDontSee('New Project'); +}); + +test('admin sees add server button on dashboard', function () { + [$user, $team] = setupDashboardUser('admin'); + + $this->actingAs($user); + session(['currentTeam' => $team]); + + createServerWithKeyForTeam($team); + + Livewire::test(Dashboard::class) + ->assertSee('New Server'); +}); + +test('member does not see add server button on dashboard', function () { + [$user, $team] = setupDashboardUser('member'); + + $this->actingAs($user); + session(['currentTeam' => $team]); + + createServerWithKeyForTeam($team); + + Livewire::test(Dashboard::class) + ->assertDontSee('New Server'); +}); diff --git a/tests/Feature/TeamDeletionTest.php b/tests/Feature/TeamDeletionTest.php new file mode 100644 index 000000000..65d6c54f1 --- /dev/null +++ b/tests/Feature/TeamDeletionTest.php @@ -0,0 +1,54 @@ + 0]); + + $this->owner = User::factory()->create(); + + // The owner's personal team (created by factory) + $this->personalTeam = $this->owner->teams()->first(); + $this->owner->teams()->updateExistingPivot($this->personalTeam->id, ['role' => 'owner']); + + // A second team to delete + $this->teamToDelete = Team::create(['name' => 'Deletable Team', 'personal_team' => false]); + $this->teamToDelete->members()->attach($this->owner->id, ['role' => 'owner']); +}); + +test('deleting a team switches session to another team without error', function () { + $this->actingAs($this->owner); + session(['currentTeam' => $this->teamToDelete]); + + Livewire::test(Index::class) + ->call('delete') + ->assertRedirect(route('team.index')); + + // Team should be deleted from the database + expect(Team::find($this->teamToDelete->id))->toBeNull(); + + // Session should now have the personal team + $sessionTeam = session('currentTeam'); + expect($sessionTeam)->not->toBeNull() + ->and($sessionTeam->id)->toBe($this->personalTeam->id); +}); + +test('refreshSession clears session when no team exists', function () { + $user = User::factory()->create(); + // Detach all teams so user has none + $user->teams()->detach(); + $this->actingAs($user); + session(['currentTeam' => null]); + + // Should not throw when no team can be resolved + refreshSession(null); + + expect(session('currentTeam'))->toBeNull(); +});