Skip to content
This repository was archived by the owner on Jun 29, 2021. It is now read-only.

Commit 7cf9eb1

Browse files
tiagofJosé Postiga
and
José Postiga
authored
Resolves #24 - Add account types and permissions (#42)
* init * Adds Question create feature resolves #25 Removes unnecessary code in AccountsStoreController. * init * done * init * done Rebased on top of feature/issue-25 since we need the AuthServiceProvider to be enabled. * Fix Remove migration 'down' function, otherwise we'd need to add doctrine/dbal dependency. Fixed exception if no user is authenticated. * Update from upstream * Move link limit validation to Policy. Other minor tweaks * Added Link limit tests for trusted and Editor users * Update CHANGELOG.md * refactor(Links): pushed policy execution to after input validation and minor code style cleanup Co-authored-by: José Postiga <[email protected]>
1 parent 0b3182e commit 7cf9eb1

15 files changed

+276
-48
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes to `laravel-portugal/api` will be documented in this file
88

99
- First version of the API documentation
1010
- A guest should be able to login and logout (#37)
11+
- Add account types and permissions (#42)
1112
- An authenticated user can post an answer to a question (#31)
1213

1314
### Changed

domains/Accounts/Controllers/AccountsStoreController.php

+4-5
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ class AccountsStoreController extends Controller
1313
{
1414
private User $user;
1515

16-
public function __construct(User $users)
16+
public function __construct(User $user)
1717
{
18-
$this->users = $users;
18+
$this->user = $user;
1919
}
2020

2121
public function __invoke(Request $request): Response
@@ -26,14 +26,13 @@ public function __invoke(Request $request): Response
2626
'password' => ['required', 'string'],
2727
]);
2828

29-
$user = new User();
30-
$user->forceFill([
29+
$this->user->forceFill([
3130
'name' => $request->input('name'),
3231
'email' => $request->input('email'),
3332
'password' => Hash::make($request->input('password')),
3433
])->save();
3534

36-
$user->notify(new VerifyEmailNotification());
35+
$this->user->notify(new VerifyEmailNotification());
3736

3837
return new Response('', Response::HTTP_NO_CONTENT);
3938
}

domains/Accounts/Database/Factories/UserFactory.php

+30
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Domains\Accounts\Database\Factories;
44

5+
use Domains\Accounts\Enums\AccountTypeEnum;
56
use Domains\Accounts\Models\User;
67
use Illuminate\Database\Eloquent\Factories\Factory;
78
use Illuminate\Support\Carbon;
@@ -14,6 +15,7 @@ class UserFactory extends Factory
1415
public function definition(): array
1516
{
1617
return [
18+
'account_type' => AccountTypeEnum::USER,
1719
'name' => $this->faker->name,
1820
'email' => $this->faker->safeEmail,
1921
'password' => Hash::make($this->faker->password(8)),
@@ -31,10 +33,38 @@ public function unverified(): self
3133
]);
3234
}
3335

36+
public function editor(): self
37+
{
38+
return $this->state([
39+
'account_type' => AccountTypeEnum::EDITOR,
40+
]);
41+
}
42+
43+
public function admin(): self
44+
{
45+
return $this->state([
46+
'account_type' => AccountTypeEnum::ADMIN,
47+
]);
48+
}
49+
3450
public function deleted(): self
3551
{
3652
return $this->state([
3753
'deleted_at' => Carbon::now(),
3854
]);
3955
}
56+
57+
public function trusted(): self
58+
{
59+
return $this->state([
60+
'trusted' => true,
61+
]);
62+
}
63+
64+
public function withRole(string $role): self
65+
{
66+
return $this->state([
67+
'account_type' => $role,
68+
]);
69+
}
4070
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
class AddAccountTypeToUsersTable extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::table('users', function (Blueprint $table) {
12+
$table->enum('account_type', ['user', 'editor', 'admin'])->default('user')->after('id');
13+
});
14+
}
15+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Domains\Accounts\Enums;
4+
5+
final class AccountTypeEnum
6+
{
7+
public const USER = 'user';
8+
public const EDITOR = 'editor';
9+
public const ADMIN = 'admin';
10+
}

domains/Accounts/Models/User.php

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Domains\Accounts\Models;
44

5+
use Domains\Accounts\Traits\HasRoles;
56
use Illuminate\Auth\Authenticatable;
67
use Illuminate\Auth\MustVerifyEmail;
78
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
@@ -18,6 +19,7 @@ class User extends Model implements AuthenticatableContract, JWTSubject
1819
use Notifiable;
1920
use SoftDeletes;
2021
use Authorizable;
22+
use HasRoles;
2123

2224
protected $hidden = [
2325
'password',
@@ -28,6 +30,11 @@ class User extends Model implements AuthenticatableContract, JWTSubject
2830
'email_verified_at' => 'date',
2931
];
3032

33+
public function isTrusted(): bool
34+
{
35+
return $this->trusted;
36+
}
37+
3138
public function getJWTIdentifier(): int
3239
{
3340
return $this->getKey();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace Domains\Accounts\Tests\Unit;
4+
5+
use Domains\Accounts\Database\Factories\UserFactory;
6+
use Domains\Accounts\Enums\AccountTypeEnum;
7+
use Domains\Accounts\Models\User;
8+
use Tests\TestCase;
9+
10+
class HasRolesTraitTest extends TestCase
11+
{
12+
private User $model;
13+
14+
protected function setUp(): void
15+
{
16+
parent::setUp();
17+
18+
$this->model = UserFactory::new()->unverified()->make();
19+
}
20+
21+
/** @test */
22+
public function it_has_user_role(): void
23+
{
24+
self::assertTrue($this->model->isOfRole(AccountTypeEnum::USER));
25+
self::assertTrue($this->model->hasRole(AccountTypeEnum::USER));
26+
27+
self::assertFalse($this->model->isOfRole(AccountTypeEnum::EDITOR));
28+
self::assertFalse($this->model->isOfRole(AccountTypeEnum::ADMIN));
29+
self::assertFalse($this->model->hasRole(AccountTypeEnum::EDITOR));
30+
self::assertFalse($this->model->hasRole(AccountTypeEnum::ADMIN));
31+
}
32+
33+
/** @test */
34+
public function it_has_editor_role(): void
35+
{
36+
$this->model = UserFactory::new()->unverified()->editor()->make();
37+
38+
self::assertTrue($this->model->isOfRole(AccountTypeEnum::EDITOR));
39+
self::assertTrue($this->model->hasRole(AccountTypeEnum::EDITOR));
40+
self::assertTrue($this->model->hasRole(AccountTypeEnum::USER));
41+
42+
self::assertFalse($this->model->isOfRole(AccountTypeEnum::USER));
43+
self::assertFalse($this->model->isOfRole(AccountTypeEnum::ADMIN));
44+
self::assertFalse($this->model->hasRole(AccountTypeEnum::ADMIN));
45+
}
46+
47+
/** @test */
48+
public function it_has_admin_role(): void
49+
{
50+
$this->model = UserFactory::new()->unverified()->admin()->make();
51+
52+
self::assertTrue($this->model->isOfRole(AccountTypeEnum::ADMIN));
53+
self::assertTrue($this->model->hasRole(AccountTypeEnum::ADMIN));
54+
self::assertTrue($this->model->hasRole(AccountTypeEnum::EDITOR));
55+
self::assertTrue($this->model->hasRole(AccountTypeEnum::USER));
56+
57+
self::assertFalse($this->model->isOfRole(AccountTypeEnum::EDITOR));
58+
self::assertFalse($this->model->isOfRole(AccountTypeEnum::USER));
59+
}
60+
}

domains/Accounts/Tests/Unit/UserModelTest.php

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Carbon\Carbon;
66
use Domains\Accounts\Database\Factories\UserFactory;
7+
use Domains\Accounts\Enums\AccountTypeEnum;
78
use Domains\Accounts\Models\User;
89
use Illuminate\Database\Eloquent\SoftDeletingScope;
910
use Tests\TestCase;
@@ -56,4 +57,10 @@ public function it_uses_timestamps(): void
5657
{
5758
self::assertTrue($this->model->usesTimestamps());
5859
}
60+
61+
/** @test */
62+
public function it_has_a_user_account_type(): void
63+
{
64+
self::assertEquals(AccountTypeEnum::USER, $this->model->account_type);
65+
}
5966
}

domains/Accounts/Traits/HasRoles.php

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Domains\Accounts\Traits;
4+
5+
use Domains\Accounts\Enums\AccountTypeEnum;
6+
7+
trait HasRoles
8+
{
9+
public function isOfRole(string $role): bool
10+
{
11+
return $this->account_type === $role;
12+
}
13+
14+
public function hasRole(string $role): bool
15+
{
16+
$roles = [];
17+
switch ($this->account_type) {
18+
case AccountTypeEnum::ADMIN:
19+
$roles[] = AccountTypeEnum::ADMIN;
20+
case AccountTypeEnum::EDITOR:
21+
$roles[] = AccountTypeEnum::EDITOR;
22+
case AccountTypeEnum::USER:
23+
$roles[] = AccountTypeEnum::USER;
24+
}
25+
26+
return in_array($role, $roles);
27+
}
28+
}

domains/Discussions/Controllers/QuestionsStoreController.php

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Domains\Discussions\Models\Question;
77
use Illuminate\Http\Request;
88
use Illuminate\Http\Response;
9-
use Illuminate\Support\Facades\Auth;
109

1110
class QuestionsStoreController extends Controller
1211
{

domains/Links/Controllers/LinksStoreController.php

+7
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
namespace Domains\Links\Controllers;
44

55
use App\Http\Controllers\Controller;
6+
use Domains\Links\Exceptions\UnapprovedLinkLimitReachedException;
67
use Domains\Links\Models\Link;
78
use Illuminate\Http\Request;
89
use Illuminate\Http\Response;
10+
use Illuminate\Support\Facades\Gate;
911

1012
class LinksStoreController extends Controller
1113
{
@@ -29,6 +31,11 @@ public function __invoke(Request $request): Response
2931
'tags.*.id' => ['required', 'integer', 'exists:tags'],
3032
]);
3133

34+
throw_unless(
35+
Gate::allows('create', [Link::class, $request->input('author_email')]),
36+
new UnapprovedLinkLimitReachedException()
37+
);
38+
3239
$link = $this->links->create([
3340
'link' => $request->input('link'),
3441
'title' => $request->input('title'),

domains/Links/LinksServiceProvider.php

+7-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use Domains\Links\Models\Link;
66
use Domains\Links\Observers\LinkObserver;
7+
use Domains\Links\Policies\LinkPolicy;
8+
use Illuminate\Support\Facades\Gate;
79
use Illuminate\Support\ServiceProvider;
810

911
class LinksServiceProvider extends ServiceProvider
@@ -14,21 +16,21 @@ public function boot(): void
1416
$this->loadConfig();
1517

1618
$this->bootRoutes();
17-
$this->bootObservers();
19+
$this->bootPolicies();
1820
}
1921

2022
private function bootRoutes(): void
2123
{
2224
$this->loadRoutesFrom(__DIR__ . '/routes.php');
2325
}
2426

25-
private function bootObservers(): void
27+
private function loadConfig(): void
2628
{
27-
Link::observe(LinkObserver::class);
29+
$this->app->configure('links');
2830
}
2931

30-
private function loadConfig(): void
32+
private function bootPolicies(): void
3133
{
32-
$this->app->configure('links');
34+
Gate::policy(Link::class, LinkPolicy::class);
3335
}
3436
}

domains/Links/Observers/LinkObserver.php

-22
This file was deleted.

domains/Links/Policies/LinkPolicy.php

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Domains\Links\Policies;
4+
5+
use Domains\Accounts\Enums\AccountTypeEnum;
6+
use Domains\Accounts\Models\User;
7+
use Domains\Links\Models\Link;
8+
use Illuminate\Auth\Access\HandlesAuthorization;
9+
10+
class LinkPolicy
11+
{
12+
use HandlesAuthorization;
13+
14+
public function create(?User $user, string $authorEmail)
15+
{
16+
if ($user && ($user->isTrusted() || $user->hasRole(AccountTypeEnum::EDITOR))) {
17+
return true;
18+
}
19+
20+
$pendingCount = Link::forAuthorWithEmail($authorEmail)
21+
->unapproved()
22+
->count();
23+
24+
return $pendingCount < config('links.max_unapproved_links');
25+
}
26+
}

0 commit comments

Comments
 (0)