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

Commit

Permalink
Resolves #24 - Add account types and permissions (#42)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
tiagof and José Postiga authored Oct 24, 2020
1 parent 0b3182e commit 7cf9eb1
Show file tree
Hide file tree
Showing 15 changed files with 276 additions and 48 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to `laravel-portugal/api` will be documented in this file

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

### Changed
Expand Down
9 changes: 4 additions & 5 deletions domains/Accounts/Controllers/AccountsStoreController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ class AccountsStoreController extends Controller
{
private User $user;

public function __construct(User $users)
public function __construct(User $user)
{
$this->users = $users;
$this->user = $user;
}

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

$user = new User();
$user->forceFill([
$this->user->forceFill([
'name' => $request->input('name'),
'email' => $request->input('email'),
'password' => Hash::make($request->input('password')),
])->save();

$user->notify(new VerifyEmailNotification());
$this->user->notify(new VerifyEmailNotification());

return new Response('', Response::HTTP_NO_CONTENT);
}
Expand Down
30 changes: 30 additions & 0 deletions domains/Accounts/Database/Factories/UserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Domains\Accounts\Database\Factories;

use Domains\Accounts\Enums\AccountTypeEnum;
use Domains\Accounts\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
Expand All @@ -14,6 +15,7 @@ class UserFactory extends Factory
public function definition(): array
{
return [
'account_type' => AccountTypeEnum::USER,
'name' => $this->faker->name,
'email' => $this->faker->safeEmail,
'password' => Hash::make($this->faker->password(8)),
Expand All @@ -31,10 +33,38 @@ public function unverified(): self
]);
}

public function editor(): self
{
return $this->state([
'account_type' => AccountTypeEnum::EDITOR,
]);
}

public function admin(): self
{
return $this->state([
'account_type' => AccountTypeEnum::ADMIN,
]);
}

public function deleted(): self
{
return $this->state([
'deleted_at' => Carbon::now(),
]);
}

public function trusted(): self
{
return $this->state([
'trusted' => true,
]);
}

public function withRole(string $role): self
{
return $this->state([
'account_type' => $role,
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddAccountTypeToUsersTable extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->enum('account_type', ['user', 'editor', 'admin'])->default('user')->after('id');
});
}
}
10 changes: 10 additions & 0 deletions domains/Accounts/Enums/AccountTypeEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Domains\Accounts\Enums;

final class AccountTypeEnum
{
public const USER = 'user';
public const EDITOR = 'editor';
public const ADMIN = 'admin';
}
7 changes: 7 additions & 0 deletions domains/Accounts/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Domains\Accounts\Models;

use Domains\Accounts\Traits\HasRoles;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
Expand All @@ -18,6 +19,7 @@ class User extends Model implements AuthenticatableContract, JWTSubject
use Notifiable;
use SoftDeletes;
use Authorizable;
use HasRoles;

protected $hidden = [
'password',
Expand All @@ -28,6 +30,11 @@ class User extends Model implements AuthenticatableContract, JWTSubject
'email_verified_at' => 'date',
];

public function isTrusted(): bool
{
return $this->trusted;
}

public function getJWTIdentifier(): int
{
return $this->getKey();
Expand Down
60 changes: 60 additions & 0 deletions domains/Accounts/Tests/Unit/HasRolesTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Domains\Accounts\Tests\Unit;

use Domains\Accounts\Database\Factories\UserFactory;
use Domains\Accounts\Enums\AccountTypeEnum;
use Domains\Accounts\Models\User;
use Tests\TestCase;

class HasRolesTraitTest extends TestCase
{
private User $model;

protected function setUp(): void
{
parent::setUp();

$this->model = UserFactory::new()->unverified()->make();
}

/** @test */
public function it_has_user_role(): void
{
self::assertTrue($this->model->isOfRole(AccountTypeEnum::USER));
self::assertTrue($this->model->hasRole(AccountTypeEnum::USER));

self::assertFalse($this->model->isOfRole(AccountTypeEnum::EDITOR));
self::assertFalse($this->model->isOfRole(AccountTypeEnum::ADMIN));
self::assertFalse($this->model->hasRole(AccountTypeEnum::EDITOR));
self::assertFalse($this->model->hasRole(AccountTypeEnum::ADMIN));
}

/** @test */
public function it_has_editor_role(): void
{
$this->model = UserFactory::new()->unverified()->editor()->make();

self::assertTrue($this->model->isOfRole(AccountTypeEnum::EDITOR));
self::assertTrue($this->model->hasRole(AccountTypeEnum::EDITOR));
self::assertTrue($this->model->hasRole(AccountTypeEnum::USER));

self::assertFalse($this->model->isOfRole(AccountTypeEnum::USER));
self::assertFalse($this->model->isOfRole(AccountTypeEnum::ADMIN));
self::assertFalse($this->model->hasRole(AccountTypeEnum::ADMIN));
}

/** @test */
public function it_has_admin_role(): void
{
$this->model = UserFactory::new()->unverified()->admin()->make();

self::assertTrue($this->model->isOfRole(AccountTypeEnum::ADMIN));
self::assertTrue($this->model->hasRole(AccountTypeEnum::ADMIN));
self::assertTrue($this->model->hasRole(AccountTypeEnum::EDITOR));
self::assertTrue($this->model->hasRole(AccountTypeEnum::USER));

self::assertFalse($this->model->isOfRole(AccountTypeEnum::EDITOR));
self::assertFalse($this->model->isOfRole(AccountTypeEnum::USER));
}
}
7 changes: 7 additions & 0 deletions domains/Accounts/Tests/Unit/UserModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Carbon\Carbon;
use Domains\Accounts\Database\Factories\UserFactory;
use Domains\Accounts\Enums\AccountTypeEnum;
use Domains\Accounts\Models\User;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Tests\TestCase;
Expand Down Expand Up @@ -56,4 +57,10 @@ public function it_uses_timestamps(): void
{
self::assertTrue($this->model->usesTimestamps());
}

/** @test */
public function it_has_a_user_account_type(): void
{
self::assertEquals(AccountTypeEnum::USER, $this->model->account_type);
}
}
28 changes: 28 additions & 0 deletions domains/Accounts/Traits/HasRoles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Domains\Accounts\Traits;

use Domains\Accounts\Enums\AccountTypeEnum;

trait HasRoles
{
public function isOfRole(string $role): bool
{
return $this->account_type === $role;
}

public function hasRole(string $role): bool
{
$roles = [];
switch ($this->account_type) {
case AccountTypeEnum::ADMIN:
$roles[] = AccountTypeEnum::ADMIN;
case AccountTypeEnum::EDITOR:
$roles[] = AccountTypeEnum::EDITOR;
case AccountTypeEnum::USER:
$roles[] = AccountTypeEnum::USER;
}

return in_array($role, $roles);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use Domains\Discussions\Models\Question;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;

class QuestionsStoreController extends Controller
{
Expand Down
7 changes: 7 additions & 0 deletions domains/Links/Controllers/LinksStoreController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
namespace Domains\Links\Controllers;

use App\Http\Controllers\Controller;
use Domains\Links\Exceptions\UnapprovedLinkLimitReachedException;
use Domains\Links\Models\Link;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Gate;

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

throw_unless(
Gate::allows('create', [Link::class, $request->input('author_email')]),
new UnapprovedLinkLimitReachedException()
);

$link = $this->links->create([
'link' => $request->input('link'),
'title' => $request->input('title'),
Expand Down
12 changes: 7 additions & 5 deletions domains/Links/LinksServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Domains\Links\Models\Link;
use Domains\Links\Observers\LinkObserver;
use Domains\Links\Policies\LinkPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

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

$this->bootRoutes();
$this->bootObservers();
$this->bootPolicies();
}

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

private function bootObservers(): void
private function loadConfig(): void
{
Link::observe(LinkObserver::class);
$this->app->configure('links');
}

private function loadConfig(): void
private function bootPolicies(): void
{
$this->app->configure('links');
Gate::policy(Link::class, LinkPolicy::class);
}
}
22 changes: 0 additions & 22 deletions domains/Links/Observers/LinkObserver.php

This file was deleted.

26 changes: 26 additions & 0 deletions domains/Links/Policies/LinkPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Domains\Links\Policies;

use Domains\Accounts\Enums\AccountTypeEnum;
use Domains\Accounts\Models\User;
use Domains\Links\Models\Link;
use Illuminate\Auth\Access\HandlesAuthorization;

class LinkPolicy
{
use HandlesAuthorization;

public function create(?User $user, string $authorEmail)
{
if ($user && ($user->isTrusted() || $user->hasRole(AccountTypeEnum::EDITOR))) {
return true;
}

$pendingCount = Link::forAuthorWithEmail($authorEmail)
->unapproved()
->count();

return $pendingCount < config('links.max_unapproved_links');
}
}
Loading

0 comments on commit 7cf9eb1

Please sign in to comment.