The Clinic Owner's role involves managing users on their team and handling the creation of doctor/staff/patient users.
So, let's create two functions—list and create users—similarly to how we did it for the teams.
First, the Policy:
php artisan make:policy UserPolicy
app/Policies/UserPolicy.php
use App\Models\User;use App\Enums\Permission;use Illuminate\Auth\Access\HandlesAuthorization; class UserPolicy{ use HandlesAuthorization; public function viewAny(User $user): bool { return $user->hasPermissionTo(Permission::LIST_USER); } public function create(User $user): bool { return $user->hasPermissionTo(Permission::CREATE_USER); }}
Now, we can use that ' viewAnyand
createin the Controller with
Gate::authorize()`, right?
But first, let's create a Form Request.
php artisan make:request StoreUserRequest
Here are the validation rules:
app/Http/Requests/StoreUserRequest.php
use Illuminate\Validation\Rules\Password;use Illuminate\Foundation\Http\FormRequest; class StoreUserRequest extends FormRequest{ public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:8', Password::defaults()], 'role_id' => ['required', 'integer', 'exists:roles,id'], ]; } public function authorize(): bool { return true; }}
Next, the Controller.
php artisan make:controller UserController
Here's the code for the methods.
app/Http/Controllers/UserController.php
use App\Enums\Role;use App\Models\User;use Illuminate\View\View;use Illuminate\Support\Facades\Gate;use Illuminate\Http\RedirectResponse;use App\Http\Requests\StoreUserRequest;use Illuminate\Database\Eloquent\Builder;use Spatie\Permission\Models\Role as RoleModel; class UserController extends Controller{ public function index(): View { Gate::authorize('viewAny', User::class); $users = User::with('roles') ->whereHas('roles', function (Builder $query) { return $query->whereIn('name', [Role::ClinicAdmin->value, Role::Doctor->value, Role::Staff->value]); }) ->whereRelation('teams', 'team_id', '=', auth()->user()->current_team_id) ->get(); return view('user.index', compact('users')); } public function create(): View { Gate::authorize('create', User::class); $roles = RoleModel::whereIn('name', [Role::ClinicAdmin->value, Role::Doctor->value, Role::Staff->value]) ->pluck('name', 'id'); return view('user.create', compact('roles')); } public function store(StoreUserRequest $request): RedirectResponse { Gate::authorize('create', User::class); $user = User::create($request->except('role_id')); $user->assignRole($request->integer('role_id')); return redirect()->route('users.index'); }}
Looks pretty self-explanatory, right? There are a few conditions in Eloquent queries, but they look similar to the TeamController
we had created earlier, using Gate::authorize()
for permissions.
Next, the route for this Controller:
routes/web.php
Route::middleware('auth')->group(function () { // ... Route::resource('teams', TeamController::class) ->only(['index', 'create', 'store']); Route::resource('users', UserController::class) ->only(['index', 'create', 'store']); });
Finally, the menu item in the top navigation, visible only to those with permissions:
resources/views/layouts/navigation.blade.php
<!-- Navigation Links --><div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"> <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> {{ __('Dashboard') }} </x-nav-link> @can(\App\Enums\Permission::LIST_TEAM) <x-nav-link :href="route('teams.index')" :active="request()->routeIs('teams.*')"> {{ __('Clinics') }} </x-nav-link> @endcan @can(\App\Enums\Permission::LIST_USER) <x-nav-link :href="route('users.index')" :active="request()->routeIs('users.*')"> {{ __('Users') }} </x-nav-link> @endcan
And that's it, here's the result, again!
However, even though we have the visual result, let's still write the automated tests for this scenario.
php artisan make:test UserTest
tests/Feature/UserTest.php
use App\Models\User;use App\Enums\Role as RoleEnum;use Illuminate\Support\Collection;use Spatie\Permission\Models\Role as RoleModel;use function Pest\Laravel\actingAs; it('allows clinic owner and admin to view users list', function () { $clinicOwner = User::factory()->clinicOwner()->create(); $clinicAdmin = User::factory()->clinicAdmin()->create(); $doctor = User::factory()->doctor()->create(); $staff = User::factory()->staff()->create(); $patient = User::factory()->patient()->create(); actingAs($clinicOwner) ->get(route('users.index')) ->assertOk() ->assertViewHas('users', function (Collection $users) use ($clinicAdmin, $doctor, $staff, $patient): bool { return $users->contains(fn (User $user) => $user->name === $clinicAdmin->name || $user->name === $doctor->name || $user->name === $staff->name ) && $users->doesntContain(fn (User $user) => $user->name === $patient->name); }); actingAs($clinicAdmin) ->get(route('users.index')) ->assertOk() ->assertViewHas('users', function (Collection $users) use ($clinicAdmin, $doctor, $staff, $patient): bool { return $users->contains(fn (User $user) => $user->name === $clinicAdmin->name || $user->name === $doctor->name || $user->name === $staff->name ) && $users->doesntContain(fn (User $user) => $user->name === $patient->name); });}); it('forbids users without access to enter users list page', function (User $user) { actingAs($user) ->get(route('users.index')) ->assertForbidden();})->with([ fn() => User::factory()->masterAdmin()->create(), fn() => User::factory()->doctor()->create(), fn() => User::factory()->staff()->create(),]); it('forbids users without access to enter create user page', function (User $user) { actingAs($user) ->get(route('users.create')) ->assertForbidden();})->with([ fn() => User::factory()->masterAdmin()->create(), fn() => User::factory()->doctor()->create(), fn() => User::factory()->staff()->create(),]); it('allows clinic owner to create a new user and assign a role', function (RoleEnum $role) { $clinicOwner = User::factory()->clinicOwner()->create(); actingAs($clinicOwner) ->post(route('users.store'), [ 'name' => 'New User', 'email' => 'new@user.com', 'password' => 'password', 'role_id' => RoleModel::where('name', $role->value)->first()->id, ]); $newUser = User::where('email', 'new@user.com')->first(); expect($newUser->hasRole($role))->toBeTrue();})->with([ RoleEnum::ClinicAdmin, RoleEnum::Doctor, RoleEnum::Staff,]); it('allows clinic admin to create a new user and assign a role', function (RoleEnum $role) { $clinicAdmin = User::factory()->clinicAdmin()->create(); actingAs($clinicAdmin) ->post(route('users.store'), [ 'name' => 'New User', 'email' => 'new@user.com', 'password' => 'password', 'role_id' => RoleModel::where('name', $role->value)->first()->id, ]); $newUser = User::where('email', 'new@user.com')->first(); expect($newUser->hasRole($role))->toBeTrue();})->with([ RoleEnum::ClinicAdmin, RoleEnum::Doctor, RoleEnum::Staff,]);
Here's the result: