feat(auth): assign default roles on verification (#508)

This commit is contained in:
paranarimasu
2023-01-22 22:26:58 -06:00
committed by GitHub
parent 3d239a624a
commit a97a1663d4
18 changed files with 461 additions and 183 deletions
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Listeners\Auth;
use App\Models\Auth\Role;
use App\Models\Auth\User;
use Illuminate\Auth\Events\Verified;
/**
* Class AssignDefaultRolesToVerifiedUser.
*/
class AssignDefaultRolesToVerifiedUser
{
/**
* Handle the event.
*
* @param Verified $event
* @return void
*/
public function handle(Verified $event): void
{
/** @var User $user */
$user = $event->user;
$defaultRoles = Role::query()
->where(Role::ATTRIBUTE_DEFAULT, true)
->get();
if ($defaultRoles->isNotEmpty()) {
$user->assignRole($defaultRoles);
}
}
}
+11
View File
@@ -12,6 +12,7 @@ use Spatie\Permission\Models\Role as BaseRole;
/**
* Class Role.
*
* @property bool $default
* @property Carbon $created_at
* @property string $guard_name
* @property int $id
@@ -25,10 +26,20 @@ class Role extends BaseRole
final public const TABLE = 'roles';
final public const ATTRIBUTE_CREATED_AT = Model::CREATED_AT;
final public const ATTRIBUTE_DEFAULT = 'default';
final public const ATTRIBUTE_ID = 'id';
final public const ATTRIBUTE_NAME = 'name';
final public const ATTRIBUTE_UPDATED_AT = Model::UPDATED_AT;
final public const RELATION_PERMISSIONS = 'permissions';
final public const RELATION_USERS = 'users';
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
Role::ATTRIBUTE_DEFAULT => 'boolean',
];
}
+10
View File
@@ -10,6 +10,7 @@ use App\Nova\Actions\Models\Auth\Role\RevokePermissionAction;
use App\Nova\Resources\BaseResource;
use Exception;
use Laravel\Nova\Fields\BelongsToMany;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
@@ -129,6 +130,15 @@ class Role extends BaseResource
->enforceMaxlength()
->showWhenPeeking(),
Boolean::make(__('nova.fields.role.default.name'), RoleModel::ATTRIBUTE_DEFAULT)
->sortable()
->nullable()
->rules(['nullable', 'boolean'])
->help(__('nova.fields.role.default.help'))
->showOnPreview()
->filterable()
->showWhenPeeking(),
BelongsToMany::make(__('nova.resources.label.permissions'), RoleModel::RELATION_PERMISSIONS, Permission::class)
->filterable(),
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use App\Models\Auth\Role;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table(Role::TABLE, function (Blueprint $table) {
$table->boolean(Role::ATTRIBUTE_DEFAULT)->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table(Role::TABLE, function (Blueprint $table) {
$table->dropColumn(Role::ATTRIBUTE_DEFAULT);
});
}
};
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Database\Seeders;
namespace Database\Seeders\Admin\Setting;
use App\Constants\Config\FlagConstants;
use App\Constants\Config\VideoConstants;
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Database\Seeders\Auth\Role;
use App\Models\Auth\Role;
/**
* Class AdminSeeder.
*/
class AdminSeeder extends RoleSeeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
/** @var Role $role */
$role = Role::findOrCreate('Admin');
// Admin Resources
$this->configureResource($role, 'announcement', $this->extendedCrudAbilities());
$this->configureResource($role, 'dump', $this->extendedCrudAbilities());
$this->configureResource($role, 'setting', $this->crudAbilities());
// Auth Resources
$this->configureResource($role, 'permission', ['view']);
$this->configureResource($role, 'role', $this->crudAbilities());
$this->configureResource($role, 'user', $this->extendedCrudAbilities());
// Billing Resources
$this->configureResource($role, 'balance', $this->extendedCrudAbilities());
$this->configureResource($role, 'transaction', $this->extendedCrudAbilities());
// List Resources
$this->configureResource($role, 'playlist', $this->extendedCrudAbilities());
$this->configureResource($role, 'playlist track', $this->extendedCrudAbilities());
// Wiki Resources
$this->configureResource($role, 'anime', $this->extendedCrudAbilities());
$this->configureResource($role, 'anime synonym', $this->extendedCrudAbilities());
$this->configureResource($role, 'anime theme', $this->extendedCrudAbilities());
$this->configureResource($role, 'anime theme entry', $this->extendedCrudAbilities());
$this->configureResource($role, 'artist', $this->extendedCrudAbilities());
$this->configureResource($role, 'audio', $this->extendedCrudAbilities());
$this->configureResource($role, 'external resource', $this->extendedCrudAbilities());
$this->configureResource($role, 'image', $this->extendedCrudAbilities());
$this->configureResource($role, 'page', $this->extendedCrudAbilities());
$this->configureResource($role, 'series', $this->extendedCrudAbilities());
$this->configureResource($role, 'song', $this->extendedCrudAbilities());
$this->configureResource($role, 'studio', $this->extendedCrudAbilities());
$this->configureResource($role, 'video', $this->extendedCrudAbilities());
$this->configureResource($role, 'video script', $this->extendedCrudAbilities());
// Special Permissions
$this->configureAbilities($role, ['view nova', 'view telescope', 'view horizon', 'bypass api rate limiter']);
}
/**
* Get extended CRUD abilities for soft-delete resource.
*
* @return array<int, string>
*/
protected function extendedCrudAbilities(): array
{
return array_merge(
$this->crudAbilities(),
[
'restore',
'force delete',
],
);
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Database\Seeders\Auth\Role;
use App\Models\Auth\Role;
/**
* Class PlaylistUserRoleSeeder.
*/
class PlaylistUserRoleSeeder extends RoleSeeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
/** @var Role $role */
$role = Role::findOrCreate('Playlist User');
// List Resources
$this->configureResource($role, 'playlist', $this->extendedCrudAbilities());
$this->configureResource($role, 'playlist track', $this->extendedCrudAbilities());
$role->default = true;
if ($role->isDirty()) {
$role->save();
}
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Database\Seeders\Auth\Role;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use Illuminate\Database\Seeder;
use Illuminate\Support\Arr;
/**
* Class RoleSeeder.
*/
class RoleSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
$this->call(AdminSeeder::class);
$this->call(WikiEditorRoleSeeder::class);
$this->call(WikiViewerRoleSeeder::class);
$this->call(PlaylistUserRoleSeeder::class);
}
/**
* Configure role with resource abilities.
*
* @param Role $role
* @param string $resource
* @param array<int, string> $abilities
* @return void
*/
protected function configureResource(Role $role, string $resource, array $abilities): void
{
$permissions = Arr::map($abilities, fn (string $ability) => Permission::findOrCreate("$ability $resource"));
$role->givePermissionTo($permissions);
}
/**
* Configure role with abilities.
*
* @param Role $role
* @param array<int, string> $abilities
* @return void
*/
protected function configureAbilities(Role $role, array $abilities): void
{
$permissions = Arr::map($abilities, fn (string $ability) => Permission::findOrCreate($ability));
$role->givePermissionTo($permissions);
}
/**
* Get CRUD abilities for resource.
*
* @return array<int, string>
*/
protected function crudAbilities(): array
{
return [
'view',
'create',
'update',
'delete',
];
}
/**
* Get extended CRUD abilities for soft-delete resource.
*
* @return array<int, string>
*/
protected function extendedCrudAbilities(): array
{
return array_merge(
$this->crudAbilities(),
[
'restore',
],
);
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Database\Seeders\Auth\Role;
use App\Models\Auth\Role;
/**
* Class WikiEditorRoleSeeder.
*/
class WikiEditorRoleSeeder extends RoleSeeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
/** @var Role $role */
$role = Role::findOrCreate('Wiki Editor');
// List Resources
$this->configureResource($role, 'playlist', $this->extendedCrudAbilities());
$this->configureResource($role, 'playlist track', $this->extendedCrudAbilities());
// Wiki Resources
$this->configureResource($role, 'anime', $this->extendedCrudAbilities());
$this->configureResource($role, 'anime synonym', $this->extendedCrudAbilities());
$this->configureResource($role, 'anime theme', $this->extendedCrudAbilities());
$this->configureResource($role, 'anime theme entry', $this->extendedCrudAbilities());
$this->configureResource($role, 'artist', $this->extendedCrudAbilities());
$this->configureResource($role, 'audio', ['view', 'update']);
$this->configureResource($role, 'external resource', $this->extendedCrudAbilities());
$this->configureResource($role, 'image', $this->extendedCrudAbilities());
$this->configureResource($role, 'page', $this->extendedCrudAbilities());
$this->configureResource($role, 'series', $this->extendedCrudAbilities());
$this->configureResource($role, 'song', $this->extendedCrudAbilities());
$this->configureResource($role, 'studio', $this->extendedCrudAbilities());
$this->configureResource($role, 'video', ['view', 'update']);
$this->configureResource($role, 'video script', ['view']);
// Special Permissions
$this->configureAbilities($role, ['view nova']);
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Database\Seeders\Auth\Role;
use App\Models\Auth\Role;
/**
* Class WikiViewerRoleSeeder.
*/
class WikiViewerRoleSeeder extends RoleSeeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
/** @var Role $role */
$role = Role::findOrCreate('Wiki Viewer');
// List Resources
$this->configureResource($role, 'playlist', $this->extendedCrudAbilities());
$this->configureResource($role, 'playlist track', $this->extendedCrudAbilities());
// Wiki Resources
$this->configureResource($role, 'anime', ['view']);
$this->configureResource($role, 'anime synonym', ['view']);
$this->configureResource($role, 'anime theme', ['view']);
$this->configureResource($role, 'anime theme entry', ['view']);
$this->configureResource($role, 'artist', ['view']);
$this->configureResource($role, 'audio', ['view']);
$this->configureResource($role, 'external resource', ['view']);
$this->configureResource($role, 'image', ['view']);
$this->configureResource($role, 'page', ['view']);
$this->configureResource($role, 'series', ['view']);
$this->configureResource($role, 'song', ['view']);
$this->configureResource($role, 'studio', ['view']);
$this->configureResource($role, 'video', ['view']);
$this->configureResource($role, 'video script', ['view']);
// Special Permissions
$this->configureAbilities($role, ['view nova']);
}
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Database\Seeders;
namespace Database\Seeders\Billing\Transaction;
use App\Actions\Repositories\Billing\Transaction\ReconcileTransactionRepositoriesAction;
use App\Repositories\DigitalOcean\Billing\DigitalOceanTransactionRepository as DigitalOceanSourceRepository;
+5 -1
View File
@@ -4,6 +4,10 @@ declare(strict_types=1);
namespace Database\Seeders;
use Database\Seeders\Auth\Role\RoleSeeder;
use Database\Seeders\Billing\Transaction\DigitalOceanTransactionSeeder;
use Database\Seeders\Wiki\Audio\AudioSeeder;
use Database\Seeders\Wiki\Video\VideoSeeder;
use Illuminate\Database\Seeder;
/**
@@ -19,7 +23,7 @@ class DatabaseSeeder extends Seeder
public function run(): void
{
$this->call(DigitalOceanTransactionSeeder::class);
$this->call(PermissionSeeder::class);
$this->call(RoleSeeder::class);
$this->call(VideoSeeder::class);
$this->call(AudioSeeder::class);
}
-177
View File
@@ -1,177 +0,0 @@
<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use Illuminate\Database\Seeder;
/**
* Class PermissionSeeder.
*/
class PermissionSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
/** @var Role $admin */
$admin = Role::findOrCreate('Admin');
/** @var Role $wikiEditor */
$wikiEditor = Role::findOrCreate('Wiki Editor');
/** @var Role $wikiViewer */
$wikiViewer = Role::findOrCreate('Wiki Viewer');
// Admin Resources
$this->configureAdminResourcePermissions($admin, 'announcement', true);
$this->configureAdminResourcePermissions($admin, 'balance', true);
$this->configureAdminResourcePermissions($admin, 'dump', true);
$this->configureAdminResourcePermissions($admin, 'permission', false);
$this->configureAdminResourcePermissions($admin, 'role', false);
$this->configureAdminResourcePermissions($admin, 'setting', false);
$this->configureAdminResourcePermissions($admin, 'transaction', true);
$this->configureAdminResourcePermissions($admin, 'user', true);
// Wiki Resources
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'anime');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'anime synonym');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'anime theme');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'anime theme entry');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'artist');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'audio');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'external resource');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'image');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'page');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'series');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'song');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'studio');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'video');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'video script');
// List Resources
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'playlist');
$this->configureWikiResourcePermissions($admin, $wikiEditor, $wikiViewer, 'playlist track');
// Special Permissions
$this->configureSpecialPermissions($admin, $wikiEditor, $wikiViewer);
}
/**
* Assign permissions for admin resources.
*
* @param Role $admin
* @param string $adminResource
* @param bool $includeSoftDeletion
* @return void
*/
protected function configureAdminResourcePermissions(Role $admin, string $adminResource, bool $includeSoftDeletion): void
{
$permissions = [];
$permissions[] = Permission::findOrCreate("view $adminResource");
if ($adminResource !== 'permission') {
$permissions[] = Permission::findOrCreate("create $adminResource");
$permissions[] = Permission::findOrCreate("update $adminResource");
$permissions[] = Permission::findOrCreate("delete $adminResource");
}
if ($includeSoftDeletion) {
$permissions[] = Permission::findOrCreate("restore $adminResource");
$permissions[] = Permission::findOrCreate("force delete $adminResource");
}
$admin->givePermissionTo($permissions);
}
/**
* Assign permissions for wiki resources.
*
* @param Role $admin
* @param Role $editor
* @param Role $viewer
* @param string $wikiResource
* @return void
*/
protected function configureWikiResourcePermissions(Role $admin, Role $editor, Role $viewer, string $wikiResource): void
{
$adminPermissions = [];
$editorPermissions = [];
$viewerPermissions = [];
$view = Permission::findOrCreate("view $wikiResource");
$adminPermissions[] = $view;
$editorPermissions[] = $view;
$viewerPermissions[] = $view;
$create = Permission::findOrCreate("create $wikiResource");
$adminPermissions[] = $create;
if ($wikiResource !== 'video' && $wikiResource !== 'audio' && $wikiResource !== 'video script') {
$editorPermissions[] = $create;
}
$update = Permission::findOrCreate("update $wikiResource");
$adminPermissions[] = $update;
if ($wikiResource !== 'video script') {
$editorPermissions[] = $update;
}
$delete = Permission::findOrCreate("delete $wikiResource");
$adminPermissions[] = $delete;
if ($wikiResource !== 'video' && $wikiResource !== 'audio' && $wikiResource !== 'video script') {
$editorPermissions[] = $delete;
}
$restore = Permission::findOrCreate("restore $wikiResource");
$adminPermissions[] = $restore;
if ($wikiResource !== 'video' && $wikiResource !== 'audio') {
$editorPermissions[] = $restore;
}
$forceDelete = Permission::findOrCreate("force delete $wikiResource");
$adminPermissions[] = $forceDelete;
$admin->givePermissionTo($adminPermissions);
$editor->givePermissionTo($editorPermissions);
$viewer->givePermissionTo($viewerPermissions);
}
/**
* Configure special permissions.
*
* @param Role $admin
* @param Role $editor
* @param Role $viewer
* @return void
*/
protected function configureSpecialPermissions(Role $admin, Role $editor, Role $viewer): void
{
$adminPermissions = [];
$editorPermissions = [];
$viewerPermissions = [];
$viewNova = Permission::findOrCreate('view nova');
$adminPermissions[] = $viewNova;
$editorPermissions[] = $viewNova;
$viewerPermissions[] = $viewNova;
$viewTelescope = Permission::findOrCreate('view telescope');
$adminPermissions[] = $viewTelescope;
$viewHorizon = Permission::findOrCreate('view horizon');
$adminPermissions[] = $viewHorizon;
$bypassApiRateLimiter = Permission::findOrCreate('bypass api rate limiter');
$adminPermissions[] = $bypassApiRateLimiter;
$admin->givePermissionTo($adminPermissions);
$editor->givePermissionTo($editorPermissions);
$viewer->givePermissionTo($viewerPermissions);
}
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Database\Seeders;
namespace Database\Seeders\Wiki\Audio;
use App\Actions\Repositories\Wiki\Audio\ReconcileAudioRepositoriesAction;
use App\Repositories\Eloquent\Wiki\AudioRepository as AudioDestinationRepository;
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Database\Seeders;
namespace Database\Seeders\Wiki\Audio;
use App\Actions\Models\Wiki\Video\Audio\BackfillAudioAction;
use App\Models\Wiki\Video;
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Database\Seeders;
namespace Database\Seeders\Wiki\Video;
use App\Actions\Repositories\Wiki\Video\ReconcileVideoRepositoriesAction;
use App\Repositories\Eloquent\Wiki\VideoRepository as VideoDestinationRepository;
+4
View File
@@ -546,6 +546,10 @@ return [
],
],
'role' => [
'default' => [
'help' => 'Should this role be assigned to new users upon email verification?',
'name' => 'Default',
],
'name' => 'Name',
],
'series' => [
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Auth;
use App\Models\Auth\Role;
use App\Models\Auth\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
use Spatie\Permission\PermissionRegistrar;
use Tests\TestCase;
/**
* Class EmailVerificationTest.
*/
class EmailVerificationTest extends TestCase
{
use WithFaker;
/**
* The Email Verification route shall assign default roles.
*
* @return void
*/
public function testAssignsDefaultRoles(): void
{
Collection::times($this->faker->randomDigitNotNull, function () {
Role::findOrCreate(Str::random());
});
$defaultRoleCount = $this->faker->randomDigitNotNull();
Collection::times($defaultRoleCount, function () {
/** @var Role $role */
$role = Role::findOrCreate(Str::random());
$role->default = true;
$role->save();
});
App::make(PermissionRegistrar::class)->forgetCachedPermissions();
$user = User::factory()->createOne([
User::ATTRIBUTE_EMAIL_VERIFIED_AT => null,
]);
$url = URL::temporarySignedRoute(
'verification.verify',
now()->addMinutes(60),
[
'id' => $user->getKey(),
'hash' => sha1($user->email),
]
);
$this->actingAs($user)->get($url);
static::assertCount($defaultRoleCount, $user->roles()->get());
}
}