Skip to content

Commit a1a6e96

Browse files
authored
Merge pull request laravel#27 from Artisans-PXM/permissions
2 parents b10e72c + 54faf4b commit a1a6e96

15 files changed

+299
-67
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Controllers\Api;
6+
7+
use App\Http\Controllers\Controller;
8+
use App\Http\Requests\Api\PermissionRequest;
9+
use App\Queries\PermissionQueries;
10+
use Illuminate\Http\JsonResponse;
11+
12+
class PermissionController extends Controller
13+
{
14+
public function __construct(
15+
protected PermissionQueries $permissionQueries
16+
) {
17+
18+
}
19+
20+
public function givePermissions(PermissionRequest $request): JsonResponse
21+
{
22+
$validatedData = $request->validated();
23+
24+
$this->permissionQueries->givePermissions($validatedData);
25+
26+
return response()->json([
27+
'success' => __('Permission given successfully.'),
28+
]);
29+
}
30+
31+
public function revokePermissions(PermissionRequest $request): JsonResponse
32+
{
33+
$validatedData = $request->validated();
34+
35+
$this->permissionQueries->revokePermissions($validatedData);
36+
37+
return response()->json([
38+
'success' => __('Permission revoked successfully.'),
39+
]);
40+
}
41+
}

app/Http/Controllers/Api/RoleController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function fetch(): AnonymousResourceCollection
2323
{
2424
$roles = $this->roleQueries->listQuery();
2525

26-
return RoleResource::collection($roles);
26+
return RoleResource::collection($roles->getCollection());
2727
}
2828

2929
public function create(RoleRequest $request): JsonResponse

app/Http/Controllers/Api/UserController.php

+2-9
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ public function __construct(
1919

2020
}
2121

22-
public function fetch(): AnonymousResourceCollection
22+
public function fetch(string $roleId = null): AnonymousResourceCollection
2323
{
24-
$users = $this->userQueries->listQuery();
24+
$users = $this->userQueries->listQuery($roleId);
2525

2626
return UserResource::collection($users->getCollection());
2727
}
@@ -61,11 +61,4 @@ public function update(UserRequest $request, string $id): JsonResponse
6161
'success' => __('User updated successfully.'),
6262
]);
6363
}
64-
65-
public function fetchByRole(string $roleId): AnonymousResourceCollection
66-
{
67-
$users = $this->userQueries->fetchByRole($roleId);
68-
69-
return UserResource::collection($users->getCollection());
70-
}
7164
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Requests\Api;
6+
7+
use App\Permission;
8+
use Illuminate\Contracts\Validation\ValidationRule;
9+
use Illuminate\Foundation\Http\FormRequest;
10+
use Illuminate\Validation\Rule;
11+
use Illuminate\Validation\Rules\In;
12+
13+
class PermissionRequest extends FormRequest
14+
{
15+
/**
16+
* Get the validation rules that apply to the request.
17+
*
18+
* @return array<string, (ValidationRule | array<int, string | In> | string)>
19+
*/
20+
public function rules(): array
21+
{
22+
return [
23+
'role' => ['required', 'string', 'uuid', 'exists:roles,id'],
24+
'permissions' => ['required', 'array'],
25+
'permissions.*' => ['required_with:permissions', 'string', Rule::in(Permission::getFeatureGates()->toArray())],
26+
];
27+
}
28+
}

app/Models/Permission.php

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Database\Eloquent\Concerns\HasUuids;
88
use Illuminate\Database\Eloquent\Factories\HasFactory;
99
use Illuminate\Database\Eloquent\Model;
10+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
1011

1112
class Permission extends Model
1213
{
@@ -19,4 +20,9 @@ class Permission extends Model
1920
* @var array<int, string>
2021
*/
2122
protected $fillable = ['role_id', 'title'];
23+
24+
public function role(): BelongsTo
25+
{
26+
return $this->belongsTo(Role::class);
27+
}
2228
}

app/Permission.php

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public function __construct(
3131
protected array $permissions = [
3232
'assign-user-roles',
3333
'dissociate-user-roles',
34+
'give-permissions',
35+
'revoke-permissions',
3436
]
3537
) {
3638

app/Queries/PermissionQueries.php

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Queries;
6+
7+
use App\Models\Permission;
8+
9+
class PermissionQueries
10+
{
11+
/**
12+
* @param array<string, string|array<int, string>> $data
13+
*/
14+
public function givePermissions(array $data): void
15+
{
16+
/** @var array<int, string> $permissions */
17+
$permissions = $data['permissions'];
18+
19+
$insertData = [];
20+
21+
foreach ($permissions as $permission) {
22+
$insertData[] = [
23+
'role_id' => $data['role'],
24+
'title' => $permission,
25+
];
26+
}
27+
28+
Permission::upsert($insertData, ['role_id', 'title']);
29+
}
30+
31+
/**
32+
* @param array<string, string|array<int, string>> $data
33+
*/
34+
public function revokePermissions(array $data): void
35+
{
36+
Permission::query()
37+
->where('role_id', $data['role'])
38+
->whereIn('title', $data['permissions'])
39+
->delete();
40+
}
41+
}

app/Queries/UserQueries.php

+6-20
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
class UserQueries extends GlobalQueries
1212
{
13-
public function listQuery(): LengthAwarePaginator
13+
public function listQuery(?string $roleId): LengthAwarePaginator
1414
{
1515
return QueryBuilder::for(User::class)
1616
->allowedFields(['first_name', 'last_name', 'username', 'email', 'created_at'])
@@ -27,6 +27,11 @@ public function listQuery(): LengthAwarePaginator
2727
'tokens:id,tokenable_id,tokenable_type,last_used_at',
2828
'roles:id,name',
2929
])
30+
->when($roleId, function ($query) use ($roleId): void {
31+
$query->whereHas('roles', function ($query) use ($roleId): void {
32+
$query->where('role_id', $roleId);
33+
});
34+
})
3035
->jsonPaginate();
3136
}
3237

@@ -62,23 +67,4 @@ public function findByEmail(string $email): ?User
6267
{
6368
return User::query()->firstWhere('email', $email);
6469
}
65-
66-
public function fetchByRole(string $roleId): LengthAwarePaginator
67-
{
68-
return QueryBuilder::for(User::class)
69-
->allowedFields(['first_name', 'last_name', 'username', 'email', 'created_at'])
70-
->allowedFilters([
71-
$this->filter('first_name'),
72-
$this->filter('last_name'),
73-
$this->filter('username'),
74-
$this->filter('email'),
75-
])
76-
->defaultSort('-created_at')
77-
->allowedSorts(['first_name', 'last_name', 'created_at'])
78-
->mergeSelect('id')
79-
->withWhereHas('roles', function ($query) use ($roleId): void {
80-
$query->where('role_id', $roleId);
81-
})
82-
->jsonPaginate();
83-
}
8470
}

routes/api.php

+7-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use App\Http\Controllers\Api\GenerateTokenController;
99
use App\Http\Controllers\Api\HierarchyController;
1010
use App\Http\Controllers\Api\LocaleController;
11+
use App\Http\Controllers\Api\PermissionController;
1112
use App\Http\Controllers\Api\PriceBookController;
1213
use App\Http\Controllers\Api\RoleController;
1314
use App\Http\Controllers\Api\RoleUserController;
@@ -25,8 +26,7 @@
2526

2627
Route::middleware(['auth:sanctum', 'set.company'])->group(function (): void {
2728
Route::controller(UserController::class)->name('users.')->prefix('users')->group(function (): void {
28-
Route::get('fetch', 'fetch')->can('fetch-users')->name('fetch');
29-
Route::get('fetch/{roleId}', 'fetchByRole')->can('fetch-users')->name('fetch_by_role');
29+
Route::get('fetch/{roleId?}', 'fetch')->can('fetch-users')->name('fetch');
3030
Route::post('create', 'create')->can('create-user')->name('create');
3131
Route::delete('{id}/delete', 'delete')->can('delete-user')->name('delete');
3232
Route::post('{id}/restore', 'restore')->can('delete-user')->name('restore');
@@ -45,6 +45,11 @@
4545
Route::post('dissociate-roles', 'dissociateRoles')->can('dissociate-user-roles')->name('dissociate_roles');
4646
});
4747

48+
Route::controller(PermissionController::class)->name('permissions.')->prefix('permissions')->group(function (): void {
49+
Route::post('give-permissions', 'givePermissions')->can('give-permissions')->name('give');
50+
Route::post('revoke-permissions', 'revokePermissions')->can('revoke-permissions')->name('revoke');
51+
});
52+
4853
Route::controller(LocaleController::class)->name('locales.')->prefix('locales')->group(function (): void {
4954
Route::get('fetch', 'fetch')->can('fetch-locales')->name('fetch');
5055
Route::post('create', 'create')->can('create-locale')->name('create');

stubs/request.stub

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class {{ class }} extends FormRequest
99
/**
1010
* Get the validation rules that apply to the request.
1111
*
12-
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
12+
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<int, string>|string>
1313
*/
1414
public function rules(): array
1515
{

tests/Feature/Api/CompanyControllerTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
->has(
2626
'0',
2727
fn (AssertableJson $json): AssertableJson => $json
28-
->where('id', ($user = $user->companies()->first())->id)
28+
->where('id', ($user = $user->companies->sortByDesc('created_at')->first())->id)
2929
->where('name', $user->name)
3030
->where('email', $user->email)
3131
->etc()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use App\Models\Permission;
6+
7+
beforeEach(function (): void {
8+
[$this->user, $this->company, $this->token] = frontendApiLoginWithUser('Super Admin');
9+
});
10+
11+
test('it can give the permissions', function (): void {
12+
$role = $this->user->roles->first();
13+
14+
Permission::factory()->for($role)->create();
15+
16+
$response = $this->withToken($this->token)->postJson(route('api.permissions.give'), [
17+
'role' => $role->id,
18+
'permissions' => ['create-user'],
19+
]);
20+
21+
$response->assertOk()->assertJsonStructure(['success']);
22+
23+
$this->assertDatabaseHas(Permission::class, [
24+
'role_id' => $role->id,
25+
'title' => 'create-user',
26+
]);
27+
});
28+
29+
test('it can revoke the permissions', function (): void {
30+
$role = $this->user->roles->first();
31+
32+
$permissions = Permission::factory(3)->for($role)->sequence(
33+
['title' => 'create-user'],
34+
['title' => 'delete-user'],
35+
['title' => 'fetch-users']
36+
)->create();
37+
38+
$response = $this->withToken($this->token)->postJson(route('api.permissions.revoke'), [
39+
'role' => $role->id,
40+
'permissions' => ['create-user', 'delete-user', 'fetch-users'],
41+
]);
42+
43+
$response->assertOk()->assertJsonStructure(['success']);
44+
45+
expect(Permission::first())->toBeNull();
46+
});

tests/Feature/Api/RoleControllerTest.php

+72-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,77 @@
22

33
declare(strict_types=1);
44

5-
test('example test', function (): void {
5+
use App\Models\Role;
6+
use Illuminate\Http\Response;
7+
use Illuminate\Testing\Fluent\AssertableJson;
68

9+
beforeEach(function (): void {
10+
[$this->user, $this->company, $this->token] = frontendApiLoginWithUser('Super Admin');
11+
});
12+
13+
test('it can fetch roles', function (): void {
14+
$response = $this->withToken($this->token)->getJson(route('api.roles.fetch'));
15+
16+
$response->assertOk()
17+
->assertJson(
18+
fn (AssertableJson $json): AssertableJson => $json
19+
->has(
20+
'data',
21+
fn (AssertableJson $json): AssertableJson => $json
22+
->has(
23+
'0',
24+
fn (AssertableJson $json): AssertableJson => $json
25+
->where('id', ($role = Role::latest()->first())->id)
26+
->where('name', $role->name)
27+
->etc()
28+
)
29+
->etc()
30+
)
31+
);
32+
});
33+
34+
test('it can create role', function (): void {
35+
$response = $this->withToken($this->token)->postJson(route('api.roles.create'), [
36+
'name' => $name = 'Access Manager',
37+
]);
38+
39+
$response->assertOk()->assertJsonStructure(['success', 'role_id']);
40+
41+
$this->assertDatabaseHas(Role::class, [
42+
'name' => $name,
43+
]);
44+
});
45+
46+
test('it can delete the role', function (): void {
47+
$role = Role::factory()->named('Access Manager')->create();
48+
49+
$response = $this->withToken($this->token)->deleteJson(route('api.roles.delete', [
50+
'id' => $role->id,
51+
]));
52+
53+
$response->assertOk()->assertJsonStructure(['success']);
54+
55+
$this->assertModelMissing($role);
56+
});
57+
58+
test('it cannot delete the role if it assign to the user', function (): void {
59+
$response = $this->withToken($this->token)->deleteJson(route('api.roles.delete', [
60+
'id' => Role::min('id'),
61+
]));
62+
63+
$response->assertStatus(Response::HTTP_NOT_ACCEPTABLE);
64+
});
65+
66+
test('it can update the role', function (): void {
67+
$response = $this->withToken($this->token)->postJson(route('api.roles.update', [
68+
'id' => Role::min('id'),
69+
]), [
70+
'name' => $name = 'Access Manager',
71+
]);
72+
73+
$response->assertOk()->assertJsonStructure(['success']);
74+
75+
$this->assertDatabaseHas(Role::class, [
76+
'name' => $name,
77+
]);
778
});

0 commit comments

Comments
 (0)