Back to Course |
Roles and Permissions in Laravel 11

Clinic Owner: Switching Between Teams

The final feature related to the Teams is for the Clinic Owner to choose the current team if they have multiple clinics.

This choice will be visible in the top-right corner of the Laravel Breeze design, like this:

Now, the code.

First, the permission of WHO can change the team? We add a method to the Policy:

app/Policies/TeamPolicy.php

class TeamPolicy
{
// ...
 
public function changeTeam(User $user): bool
{
return $user->hasPermissionTo(Permission::SWITCH_TEAM);
}
}

Next is the Controller method, where we will use that changeTeam immediately. We will add that method into the same TeamController:

app/Http/Controllers/TeamController.php:

use App\Enums\Role;
use Symfony\Component\HttpFoundation\Response;
 
class TeamController extends Controller
{
public function changeCurrentTeam(int $teamId)
{
Gate::authorize('changeTeam', Team::class);
 
$team = auth()->user()->teams()->findOrFail($teamId);
 
if (! auth()->user()->belongsToTeam($team)) {
abort(Response::HTTP_FORBIDDEN);
}
 
// Change team
auth()->user()->update(['current_team_id' => $team->id]);
setPermissionsTeamId($team->id);
auth()->user()->unsetRelation('roles')->unsetRelation('permissions');
 
return redirect(route('dashboard'), Response::HTTP_SEE_OTHER);
}
}

Next, we add the Route for that method.

routes/web.php

Route::middleware('auth')->group(function () {
 
Route::resource('teams', TeamController::class)->only(['index', 'create', 'store']);
Route::get('team/change/{teamId}',
[TeamController::class, 'changeCurrentTeam'])
->name('team.change');
});

Finally, the front-end link to that Route. We just mimic the front-end code of the Profile dropdown menu.

resources/views/layouts/navigation.blade.php

<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6">
@can(\App\Enums\Permission::SWITCH_TEAM)
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->currentTeam->name }}</div>
 
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>
 
<x-slot name="content">
@if (Auth::user()->teams()->count() > 1)
@foreach (auth()->user()->teams as $team)
<x-dropdown-link :href="route('team.change', $team->id)" @class(['font-bold' => auth()->user()->current_team_id == $team->id])>
{{ $team->name }}
</x-dropdown-link>
@endforeach
@else
<span class="block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out">
No other teams
</span>
@endif
</x-slot>
</x-dropdown>
@endcan
 
<x-dropdown align="right" width="48">
... (identical Breeze dropdown for "Edit Profile" and "Logout")
</x-dropdown>
</div>

Now, the tests. We will add a few methods to the existing TeamTest.

tests/Feature/TeamTest.php:

use App\Models\User;
use App\Models\Team;
use App\Enums\Role as RoleEnum;
use Spatie\Permission\Models\Role as RoleModel;
use function Pest\Laravel\actingAs;
 
it('allows clinic owner to change team', function () {
$clinicOwner = User::factory()->clinicOwner()->create();
$secondTeam = Team::factory()->create();
 
$clinicOwner->teams()->attach($secondTeam->id, [
'role_id' => RoleModel::where('name', RoleEnum::ClinicOwner->value)->first()->id,
'model_type' => $clinicOwner->getMorphClass(),
]);
 
actingAs($clinicOwner)
->get(route('team.change', $secondTeam->id));
 
expect($clinicOwner->refresh()->current_team_id)->toBe($secondTeam->id);
});
 
it('does not allow user to change team if user is not in the team', function () {
$clinicOwner = User::factory()->clinicOwner()->create();
$secondTeam = Team::factory()->create();
 
actingAs($clinicOwner)
->get(route('team.change', $secondTeam->id))
->assertNotFound();
 
expect($clinicOwner->refresh()->current_team_id)->toBe($clinicOwner->current_team_id);
});
 
it('does not allow to change team for user without switch team permissions', function (User $user) {
$team = Team::factory()->create();
 
actingAs($user)
->get(route('team.change', $team->id))
->assertForbidden();
})->with([
fn () => User::factory()->masterAdmin()->create(),
fn () => User::factory()->clinicAdmin()->create(),
fn () => User::factory()->staff()->create(),
fn () => User::factory()->doctor()->create(),
fn () => User::factory()->patient()->create(),
]);

The full result of the TeamTest:


Helper for Testing: Spatie Login Link

While preparing this project, it was annoying to log out and log in with different users constantly. So, there's a Laravel package spatie/laravel-login-link that makes this task much easier!

The result is this: instead of entering email/password, we just need to click one of the links above the form:

To achieve that, we need to do this:

composer require spatie/laravel-login-link
php artisan vendor:publish --tag="login-link-config"

And then we added a few links to the login page:

resources/views/auth/login.blade.php

<x-guest-layout>
@env('local')
<div class="space-y-2 mb-4">
<x-login-link email="master@admin.com" label="Login as master admin"/>
<x-login-link email="owner@clinic.com" label="Login as clinic owner"/>
<x-login-link email="admin@clinic.com" label="Login as clinic admin"/>
<x-login-link email="staff@clinic.com" label="Login as staff"/>
<x-login-link email="user@clinic.com" label="Login as patient"/>
</div>
@endenv
 
... The rest of the login form

And that's it!

Notice: Ensure these links are seen only in the local environment. Otherwise, it may be a security issue.


Great, we have the Team/Clinic management! Now, let's go to the User level.