diff --git a/app/Listeners/Auth/AssignDefaultRolesToVerifiedUser.php b/app/Listeners/Auth/AssignDefaultRolesToVerifiedUser.php new file mode 100644 index 000000000..47a880e8d --- /dev/null +++ b/app/Listeners/Auth/AssignDefaultRolesToVerifiedUser.php @@ -0,0 +1,35 @@ +user; + + $defaultRoles = Role::query() + ->where(Role::ATTRIBUTE_DEFAULT, true) + ->get(); + + if ($defaultRoles->isNotEmpty()) { + $user->assignRole($defaultRoles); + } + } +} diff --git a/app/Models/Auth/Role.php b/app/Models/Auth/Role.php index 7139f7527..32aa4b80a 100644 --- a/app/Models/Auth/Role.php +++ b/app/Models/Auth/Role.php @@ -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 + */ + protected $casts = [ + Role::ATTRIBUTE_DEFAULT => 'boolean', + ]; } diff --git a/app/Nova/Resources/Auth/Role.php b/app/Nova/Resources/Auth/Role.php index 712d4c4fc..d464ea568 100644 --- a/app/Nova/Resources/Auth/Role.php +++ b/app/Nova/Resources/Auth/Role.php @@ -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(), diff --git a/database/migrations/2023_01_22_040621_add_default_attribute_to_role.php b/database/migrations/2023_01_22_040621_add_default_attribute_to_role.php new file mode 100644 index 000000000..ee206fc06 --- /dev/null +++ b/database/migrations/2023_01_22_040621_add_default_attribute_to_role.php @@ -0,0 +1,35 @@ +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); + }); + } +}; diff --git a/database/seeders/SettingSeeder.php b/database/seeders/Admin/Setting/SettingSeeder.php similarity index 98% rename from database/seeders/SettingSeeder.php rename to database/seeders/Admin/Setting/SettingSeeder.php index d32b97f0e..b8e1be0cc 100644 --- a/database/seeders/SettingSeeder.php +++ b/database/seeders/Admin/Setting/SettingSeeder.php @@ -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; diff --git a/database/seeders/Auth/Role/AdminSeeder.php b/database/seeders/Auth/Role/AdminSeeder.php new file mode 100644 index 000000000..cdf81db0f --- /dev/null +++ b/database/seeders/Auth/Role/AdminSeeder.php @@ -0,0 +1,77 @@ +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 + */ + protected function extendedCrudAbilities(): array + { + return array_merge( + $this->crudAbilities(), + [ + 'restore', + 'force delete', + ], + ); + } +} diff --git a/database/seeders/Auth/Role/PlaylistUserRoleSeeder.php b/database/seeders/Auth/Role/PlaylistUserRoleSeeder.php new file mode 100644 index 000000000..91d0838fe --- /dev/null +++ b/database/seeders/Auth/Role/PlaylistUserRoleSeeder.php @@ -0,0 +1,33 @@ +configureResource($role, 'playlist', $this->extendedCrudAbilities()); + $this->configureResource($role, 'playlist track', $this->extendedCrudAbilities()); + + $role->default = true; + if ($role->isDirty()) { + $role->save(); + } + } +} diff --git a/database/seeders/Auth/Role/RoleSeeder.php b/database/seeders/Auth/Role/RoleSeeder.php new file mode 100644 index 000000000..3f2523730 --- /dev/null +++ b/database/seeders/Auth/Role/RoleSeeder.php @@ -0,0 +1,88 @@ +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 $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 $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 + */ + protected function crudAbilities(): array + { + return [ + 'view', + 'create', + 'update', + 'delete', + ]; + } + + /** + * Get extended CRUD abilities for soft-delete resource. + * + * @return array + */ + protected function extendedCrudAbilities(): array + { + return array_merge( + $this->crudAbilities(), + [ + 'restore', + ], + ); + } +} diff --git a/database/seeders/Auth/Role/WikiEditorRoleSeeder.php b/database/seeders/Auth/Role/WikiEditorRoleSeeder.php new file mode 100644 index 000000000..088f0bb67 --- /dev/null +++ b/database/seeders/Auth/Role/WikiEditorRoleSeeder.php @@ -0,0 +1,47 @@ +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']); + } +} diff --git a/database/seeders/Auth/Role/WikiViewerRoleSeeder.php b/database/seeders/Auth/Role/WikiViewerRoleSeeder.php new file mode 100644 index 000000000..1ce07eb03 --- /dev/null +++ b/database/seeders/Auth/Role/WikiViewerRoleSeeder.php @@ -0,0 +1,47 @@ +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']); + } +} diff --git a/database/seeders/DigitalOceanTransactionSeeder.php b/database/seeders/Billing/Transaction/DigitalOceanTransactionSeeder.php similarity index 95% rename from database/seeders/DigitalOceanTransactionSeeder.php rename to database/seeders/Billing/Transaction/DigitalOceanTransactionSeeder.php index 0d44371e0..c95805d1a 100644 --- a/database/seeders/DigitalOceanTransactionSeeder.php +++ b/database/seeders/Billing/Transaction/DigitalOceanTransactionSeeder.php @@ -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; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index a1b63c63f..4fa8c0fbf 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -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); } diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php deleted file mode 100644 index 5f79f4f04..000000000 --- a/database/seeders/PermissionSeeder.php +++ /dev/null @@ -1,177 +0,0 @@ -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); - } -} diff --git a/database/seeders/AudioSeeder.php b/database/seeders/Wiki/Audio/AudioSeeder.php similarity index 95% rename from database/seeders/AudioSeeder.php rename to database/seeders/Wiki/Audio/AudioSeeder.php index 7d7b825ad..af280a3ca 100644 --- a/database/seeders/AudioSeeder.php +++ b/database/seeders/Wiki/Audio/AudioSeeder.php @@ -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; diff --git a/database/seeders/BackfillAudioSeeder.php b/database/seeders/Wiki/Audio/BackfillAudioSeeder.php similarity index 94% rename from database/seeders/BackfillAudioSeeder.php rename to database/seeders/Wiki/Audio/BackfillAudioSeeder.php index d3887c467..1b4af074f 100644 --- a/database/seeders/BackfillAudioSeeder.php +++ b/database/seeders/Wiki/Audio/BackfillAudioSeeder.php @@ -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; diff --git a/database/seeders/VideoSeeder.php b/database/seeders/Wiki/Video/VideoSeeder.php similarity index 95% rename from database/seeders/VideoSeeder.php rename to database/seeders/Wiki/Video/VideoSeeder.php index 9559c50bc..982763c0a 100644 --- a/database/seeders/VideoSeeder.php +++ b/database/seeders/Wiki/Video/VideoSeeder.php @@ -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; diff --git a/lang/en/nova.php b/lang/en/nova.php index 62b8e5067..ebf5f1ece 100644 --- a/lang/en/nova.php +++ b/lang/en/nova.php @@ -546,6 +546,10 @@ return [ ], ], 'role' => [ + 'default' => [ + 'help' => 'Should this role be assigned to new users upon email verification?', + 'name' => 'Default', + ], 'name' => 'Name', ], 'series' => [ diff --git a/tests/Feature/Http/Auth/EmailVerificationTest.php b/tests/Feature/Http/Auth/EmailVerificationTest.php new file mode 100644 index 000000000..199682e5c --- /dev/null +++ b/tests/Feature/Http/Auth/EmailVerificationTest.php @@ -0,0 +1,64 @@ +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()); + } +}