Download as pdf or txt
Download as pdf or txt
You are on page 1of 20

05/13 Company Owner: Manages Users

Now that the administrator can add users to the company, we need to
01 1716 words
implement a feature where the can add users to the
02 1702 words
company themselves.
03 1041 words

04 2333 words

05 1708 words

06 2536 words

First, let's add `SoftDeletes` for the User Model if someone accidentally 07 4852 words

deletes a user. I personally do that for almost all DB tables in Laravel, my


08 3205 words
experience showed that such "just in case" paid off in case of emergencies
09 3332 words
too often.
10 1663 words

11 976 words
php artisan make:migration "add soft deletes to users table"
12 933 words

13 644 words

:
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->softDeletes();
});
}

use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Authenticatable


{
use HasApiTokens, HasFactory, Notifiable;
use SoftDeletes;

// ...
}

Because of this added feature, the test


`test_user_can_delete_their_account` from Laravel Breeze is now broken.

Let's �x it.
:

class ProfileTest extends TestCase


{
public function test_user_can_delete_their_account(): void
{
$user = User::factory()->create();

$response = $this
->actingAs($user)
->delete('/profile', [
'password' => 'password',
]);

$response
->assertSessionHasNoErrors()
->assertRedirect('/');

$this->assertGuest();
$this->assertNull($user->fresh());
$this->assertSoftDeleted($user->fresh());
}
}
Great, now it's �xed!

> php artisan test --filter=test_user_can_delete_their_account

PASS Tests\Feature\ProfileTest
✓ user can delete their account 0.13s

Tests: 1 passed (5 assertions)


Duration: 0.15s

Now, let's move on to the main feature. First, let's show the new item
`Administrators` in the navigation, which will be visible only for users with

the role of `Company Owner`.

: I know it sounds a bit confusing: internally we call those people


"Company Owners" role but for them visually a better understandable word
is "Administrators".
Let's add this new menu item after the menu "Companies". For permission
check, I will just add a simple `@if` to check for the `role_id`.

// ...
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')
{{ __('Dashboard') }}
</x-nav-link>
@if(auth()->user()->role_id === \App\Enums\Role::ADMINISTRATOR->value)
<x-nav-link :href="route('companies.index')" :active="request()->routeIs('companies.
{{ __('Companies') }}
</x-nav-link>
@endif
@if(auth()->user()->role_id === \App\Enums\Role::COMPANY_OWNER->value)
<x-nav-link :href="route('companies.users.index', auth()->user()->company_id)
{{ __('Administrators') }}
</x-nav-link>
@endif
</div>
// ...
Now that we have the navigation link, let's implement the backend part.

We do have the CRUD Controller from the last lesson, but now we need to
work on the permissions to "open up" that CRUD to another role.

So, �rst, let's create a and register it in the `AuthServiceProvider`.

php artisan make:policy CompanyUserPolicy --model=Company

use App\Models\Company;
use App\Policies\CompanyUserPolicy;

class AuthServiceProvider extends ServiceProvider


{
protected $policies = [
Company::class => CompanyUserPolicy::class,
];

// ...
}

The Policy class will contain methods to check various permissions:

• viewAny

• create

• update

• delete

And we will allow those actions based on user's role `Company Owner` and
their company ID.

But for the `administrator` role, we need to just allow . So, I


remembered the `before` method. In this method, we will just
return `true` if the user has the role of `administrator`.

So, the whole policy code is below.

use App\Enums\Role;
use App\Models\User;
use App\Models\Company;

class CompanyUserPolicy
{
public function before(User $user): bool|null
{
if ($user->role_id === Role::ADMINISTRATOR->value) {
return true;
}

return null;
}

public function viewAny(User $user, Company $company): bool


{
return $user->role_id === Role::COMPANY_OWNER->value && $user->company_id
}
public function create(User $user, Company $company): bool
{
return $user->role_id === Role::COMPANY_OWNER->value && $user->company_id
}

public function update(User $user, Company $company): bool


{
return $user->role_id === Role::COMPANY_OWNER->value && $user->company_id
}

public function delete(User $user, Company $company): bool


{
return $user->role_id === Role::COMPANY_OWNER->value && $user->company_id
}
}

Next, in the `CompanyUserController`, we need to do the `authorize` check


for each CRUD action. There are a couple of ways to do that, but I will use
the `authorize` method.

:
class CompanyUserController extends Controller
{
public function index(Company $company)
{
$this->authorize('viewAny', $company);

// ...
}

public function create(Company $company)


{
$this->authorize('create', $company);

// ...
}

public function store(StoreUserRequest $request, Company $company)


{
$this->authorize('create', $company);

// ...
}

public function edit(Company $company, User $user)


{
$this->authorize('update', $company);
// ...
}

public function update(UpdateUserRequest $request, Company $company, User


{
$this->authorize('update', $company);

// ...
}

public function destroy(Company $company, User $user)


{
$this->authorize('delete', $company);

// ...
}
}

Great! Now users with the `Company Owner` role can create new users for
their company and cannot do any CRUD actions for other companies.
So now we made some changes to the `CompanyUserController` and added
additional authorization. First, let's check if we didn't break anything for the
users with the `administrator` role.

> php artisan test --filter=CompanyUserTest

PASS Tests\Feature\CompanyUserTest
✓ admin can access company users page 0.09s
✓ admin can create user for a company 0.02s
✓ admin can edit user for a company 0.01s
✓ admin can delete user for a company 0.01s

Tests: 4 passed (10 assertions)


Duration: 0.16s

Great! All tests are green.

Now let's add more tests to the `CompanyUserTest`. We will check if the user
with the `Company Owner` role can do CRUD actions for his company and
cannot do any for other companies.
Before adding the tests, we need to add another for the
`Company Owner` role.

class UserFactory extends Factory


{
// ...

public function companyOwner(): static


{
return $this->state(fn (array $attributes) => [
'role_id' => Role::COMPANY_OWNER->value,
]);
}
}

And the tests themselves.

class CompanyUserTest extends TestCase


{
// ...

public function test_company_owner_can_view_his_companies_users()


{
$company = Company::factory()->create();
$user = User::factory()->companyOwner()->create(['company_id' => $company
$secondUser = User::factory()->companyOwner()->create(['company_id

$response = $this->actingAs($user)->get(route('companies.users.index

$response->assertOk()
->assertSeeText($secondUser->name);
}

public function test_company_owner_cannot_view_other_companies_users()


{
$company = Company::factory()->create();
$company2 = Company::factory()->create();
$user = User::factory()->companyOwner()->create(['company_id' => $company

$response = $this->actingAs($user)->get(route('companies.users.index

$response->assertForbidden();
}

public function test_company_owner_can_create_user_to_his_company()


{
$company = Company::factory()->create();
$user = User::factory()->companyOwner()->create(['company_id' => $company

$response = $this->actingAs($user)->post(route('companies.users.store
'name' => 'test user',
'email' => 'test@test.com',
'password' => 'password',
]);

$response->assertRedirect(route('companies.users.index', $company->id

$this->assertDatabaseHas('users', [
'name' => 'test user',
'email' => 'test@test.com',
'company_id' => $company->id,
]);
}

public function test_company_owner_cannot_create_user_to_other_company


{
$company = Company::factory()->create();
$company2 = Company::factory()->create();
$user = User::factory()->companyOwner()->create(['company_id' => $company

$response = $this->actingAs($user)->post(route('companies.users.store
'name' => 'test user',
'email' => 'test@test.com',
'password' => 'password',
]);

$response->assertForbidden();
}

public function test_company_owner_can_edit_user_for_his_company()


{
$company = Company::factory()->create();
$user = User::factory()->companyOwner()->create(['company_id' => $company

$response = $this->actingAs($user)->put(route('companies.users.update
'name' => 'updated user',
'email' => 'test@update.com',
]);

$response->assertRedirect(route('companies.users.index', $company->id

$this->assertDatabaseHas('users', [
'name' => 'updated user',
'email' => 'test@update.com',
'company_id' => $company->id,
]);
}

public function test_company_owner_cannot_edit_user_for_other_company()


{
$company = Company::factory()->create();
$company2 = Company::factory()->create();
$user = User::factory()->companyOwner()->create(['company_id' => $company

$response = $this->actingAs($user)->put(route('companies.users.update
'name' => 'updated user',
'email' => 'test@update.com',
]);

$response->assertForbidden();
}

public function test_company_owner_can_delete_user_for_his_company()


{
$company = Company::factory()->create();
$user = User::factory()->companyOwner()->create(['company_id' => $company

$response = $this->actingAs($user)->delete(route('companies.users.update

$response->assertRedirect(route('companies.users.index', $company->id

$this->assertDatabaseMissing('users', [
'name' => 'updated user',
'email' => 'test@update.com',
]);
}

public function test_company_owner_cannot_delete_user_for_other_company


{
$company = Company::factory()->create();
$company2 = Company::factory()->create();
$user = User::factory()->companyOwner()->create(['company_id' => $company

$response = $this->actingAs($user)->delete(route('companies.users.update

$response->assertForbidden();
}
}

Good. All the tests passed!

Previous: Admin: Managing Users Next Lesson: Managing Guides


E-mail address

You can unsubscribe at any time. You'll also get -20% off my courses!

© 2023 Laravel Daily · info@laraveldaily.com

You might also like